Chapter 09 — Front-End Fundamentals

Sass

CSS has limits. You can’t break it into reusable files without multiple HTTP requests. You can’t write logic. Variables exist now (as covered in Chapter 5), but they can’t be manipulated at build time. Sass removes those limits.

Sass is a CSS preprocessor. You write .scss files with an extended syntax, and the Sass compiler outputs regular CSS. The browser never sees Sass. It sees the compiled result. Your Gulp pipeline handles the compilation step.

The version of Sass you want is Dart Sass, the current reference implementation. The older C++ implementation, node-sass, is deprecated. When you install the sass package from npm, you’re getting Dart Sass. Don’t install node-sass.


Install the dependencies

bash~/tutorials/
npm install --save-dev sass gulp-sass

sass is the compiler. gulp-sass is the Gulp plugin that connects it to your pipeline.


Restructure your styles

Create a scss/ folder inside src/ and set it up with partials. A partial is a Sass file whose name starts with an underscore. The underscore tells the compiler not to output it directly. Partials are imported into a main file that does get compiled.

bash~/tutorials/
mkdir src/scss
touch src/scss/main.scss
touch src/scss/_tokens.scss
touch src/scss/_reset.scss
touch src/scss/_base.scss
touch src/scss/_nav.scss
touch src/scss/_footer.scss

This file structure maps to the concerns in your stylesheet: tokens, reset, base typography, and individual component files. As a project grows, this separation keeps things manageable. You can find what you’re looking for without scrolling through a single 400-line file.

Delete src/style.css. You won’t need it anymore.


Variables

Sass variables store values and work at compile time. Define them with $:

scss~/tutorials/
$color-text: #1a1a1a;
$font-body: Georgia, serif;
$spacing-md: 24px;

They look similar to CSS custom properties from Chapter 5, but they work differently. Sass variables are resolved at compile time and disappear from the output. CSS custom properties survive into the browser and can be changed with JavaScript or overridden in media queries at runtime.

In practice, both have a place. Use Sass variables for values that only matter during compilation: breakpoint widths used in @mixin logic, Sass loops, or color functions. Use CSS custom properties for values the browser needs to know about: anything you’d adjust in a media query or override in a component.

For this project, you’ll use CSS custom properties for the design token system (as you already have), and Sass variables where they simplify compilation logic.


Nesting

Sass lets you nest selectors to reflect the HTML structure:

scss~/tutorials/
nav {
  font-family: var(--font-ui);

  ul {
    list-style: none;
    display: flex;
  }

  a {
    color: var(--color-accent);
    text-decoration: none;

    &:hover {
      text-decoration: underline;
    }
  }
}

The & refers to the parent selector. &:hover compiles to nav a:hover.

Nesting is useful but easy to abuse. Keep it to two or three levels deep. Deeper than that and the compiled output becomes over-specified, which brings you back to the specificity problems from Chapter 5. The rule of thumb: nest to reflect the DOM structure, not to organize your file.


@use and partials

The modern way to bring partials together is @use. It replaces the older @import, which is deprecated and will eventually be removed.

scss~/tutorials/
// main.scss
@use 'tokens';
@use 'reset';
@use 'base';
@use 'nav';
@use 'footer';

When you @use a partial, you omit the leading underscore and the .scss extension. Sass resolves the file automatically.

@use is also scoped: variables and mixins from a partial are namespaced to that file. If _tokens.scss defines $spacing-md, you reference it as tokens.$spacing-md in another file. This prevents naming collisions as projects grow.


Mixins

A mixin is a reusable block of styles, optionally with arguments:

scss~/tutorials/
// _tokens.scss
@mixin respond-to($breakpoint) {
  @if $breakpoint == 'md' {
    @media (max-width: #{$bp-md}) { @content; }
  } @else if $breakpoint == 'sm' {
    @media (max-width: #{$bp-sm}) { @content; }
  }
}

Use it with @include:

scss~/tutorials/
// _nav.scss
@use 'tokens' as t;

nav ul {
  display: flex;
  gap: var(--spacing-md);

  @include t.respond-to('sm') {
    flex-direction: column;
  }
}

This is the pattern for responsive styles: define your breakpoints once in a mixin, use them everywhere. Change the breakpoint value in one place and every instance updates.


Convert your styles

Move the CSS from Chapter 5 into the new partial structure.

src/scss/_tokens.scss

scss~/tutorials/
:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-muted: #555555;
  --color-accent: #2a6496;
  --font-body: Georgia, serif;
  --font-ui: system-ui, sans-serif;
  --spacing-sm: 12px;
  --spacing-md: 24px;
  --spacing-lg: 48px;
  --max-width: 720px;
}

// Sass variables for build-time use
$bp-md: 768px;
$bp-sm: 390px;

@mixin respond-to($breakpoint) {
  @if $breakpoint == 'md' {
    @media (max-width: #{$bp-md}) { @content; }
  } @else if $breakpoint == 'sm' {
    @media (max-width: #{$bp-sm}) { @content; }
  }
}

src/scss/_reset.scss

scss~/tutorials/
*,
*::before,
*::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

src/scss/_base.scss

scss~/tutorials/
body {
  background-color: var(--color-bg);
  color: var(--color-text);
  font-family: var(--font-body);
  line-height: 1.7;
  padding: var(--spacing-lg) var(--spacing-md);
}

main {
  max-width: var(--max-width);
  margin: 0 auto;
}

h1 {
  font-size: 2rem;
  line-height: 1.2;
  margin-bottom: var(--spacing-md);
}

h2 {
  font-size: 1.4rem;
  line-height: 1.3;
  margin-top: var(--spacing-lg);
  margin-bottom: var(--spacing-sm);
}

p {
  margin-bottom: var(--spacing-md);
  color: var(--color-muted);
}

ul,
ol {
  padding-left: var(--spacing-md);
  margin-bottom: var(--spacing-md);
}

li {
  margin-bottom: var(--spacing-sm);
}

.is-hidden {
  display: none;
}

src/scss/_nav.scss

scss~/tutorials/
@use 'tokens' as t;

nav {
  font-family: var(--font-ui);
  margin-bottom: var(--spacing-lg);

  ul {
    list-style: none;
    display: flex;
    gap: var(--spacing-md);

    @include t.respond-to('sm') {
      flex-direction: column;
      gap: var(--spacing-sm);
    }
  }

  a {
    color: var(--color-accent);
    text-decoration: none;

    &:hover {
      text-decoration: underline;
    }
  }
}

src/scss/_footer.scss

scss~/tutorials/
footer {
  margin-top: var(--spacing-lg);
  padding-top: var(--spacing-md);
  border-top: 1px solid #dddddd;
  font-family: var(--font-ui);
  font-size: 0.875rem;
  color: var(--color-muted);
}

src/scss/main.scss

scss~/tutorials/
@use 'tokens';
@use 'reset';
@use 'base';
@use 'nav';
@use 'footer';

Update the Gulp task

Open gulpfile.js and update the CSS task to compile Sass instead of copying plain CSS:

js~/tutorials/
const { src, dest, watch, series, parallel } = require('gulp');
const browserSync = require('browser-sync').create();
const sass = require('gulp-sass')(require('sass'));

function html() {
  return src('src/**/*.html')
    .pipe(dest('dist/'));
}

function css() {
  return src('src/scss/main.scss')
    .pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError))
    .pipe(dest('dist/css/'));
}

function js() {
  return src('src/**/*.js')
    .pipe(dest('dist/'));
}

function serve() {
  browserSync.init({
    server: {
      baseDir: './dist',
    },
  });

  watch('src/**/*.html', series(html, reload));
  watch('src/scss/**/*.scss', series(css, reload));
  watch('src/**/*.js', series(js, reload));
}

function reload(done) {
  browserSync.reload();
  done();
}

exports.build = parallel(html, css, js);
exports.default = series(parallel(html, css, js), serve);

The on('error', sass.logError) handler is important: without it, a Sass error kills the Gulp watch entirely. With it, the error gets logged to the terminal and Gulp keeps running.

Update the <link> in index.html to point to the compiled output:

html~/tutorials/
<link rel="stylesheet" href="css/main.css" />

Run gulp. The Sass compiles to dist/css/main.css and the browser reloads. The page should look identical to how it did at the end of Chapter 5. That’s correct. The work was in the structure.


Commit your work

bash~/tutorials/
git add .
git commit -m "Convert CSS to Sass with partial structure"

What you now know

You can organize styles into partials, write responsive mixins, and compile Sass through a Gulp pipeline. The file structure you’ve built here is the same structure the WordPress theme will use. In Chapter 10 you’ll apply the same concept to JavaScript, breaking main.js into modules.

Reference