TH The Sass Way
Sass & SCSS

The Complete Guide to Sass/SCSS: From Setup to Scalable Stylesheets

The Complete Guide to Sass/SCSS: From Setup to Scalable Stylesheets
tldrSass compiles SCSS into browser-ready CSS and adds build-time variables, modules, mixins, functions, maps, and loops. For a Node project, install the current `sass` package as a development dependency, invoke it through an npm script or `npx sass`, and organize new code with `@use` and `@forward`. Avoid new Sass `@import` rules, global built-in functions, legacy color helpers such as `darken()`, and slash division; they are deprecated in current Dart Sass. Use CSS custom properties for values that must change in the browser.

Sass in one sentence

Sass is a stylesheet language that compiles to CSS. In SCSS syntax, you can write ordinary CSS plus build-time variables, modules, mixins, functions, maps, loops, and nesting; the browser receives the generated CSS, not your .scss files.

This tutorial uses the current Dart Sass 1.x workflow and the module system: @use and @forward. As of August 30, 2026, the official documentation lists Dart Sass 1.103.1. Verify your installed version with the CLI and let the project lockfile record its actual dependency.

If you want a gentler definition first, read What is Sass?. If .sass and .scss sound like two sequels released in the wrong order, our Sass versus SCSS guide explains the syntax choice.

Install a project-local compiler

For a Node project, install the official sass package as a development dependency:

npm install --save-dev sass

Then put the commands in package.json so the project uses its local dependency consistently:

{
  "scripts": {
    "css:build": "sass scss/main.scss public/main.css --style=compressed",
    "css:dev": "sass --watch scss/main.scss public/main.css"
  }
}

Run them with:

npm run css:dev
npm run css:build
npx sass --version

An npm script automatically exposes local package executables. Outside a script, npx sass makes the local choice explicit. Installing sass locally and then assuming a bare global sass command is the same version is how “works on my machine” acquires a stylesheet department.

Use Dart Sass for new work. Ruby Sass reached end-of-life in 2019, LibSass is deprecated, and Node Sass reached end-of-life in 2024; do not start a new project with node-sass. The Sass team also maintains sass-embedded, which wraps the Dart VM and can be faster, and publishes standalone executables. Your framework may already integrate Sass. Check its current documentation before adding a parallel compiler that writes the same file from a second direction.

The CLI supports one-file mode:

npx sass scss/main.scss public/main.css

and directory mapping:

npx sass scss:public/css

Useful flags include --watch, --update (compile only stylesheets whose dependencies are newer than their CSS), --style=compressed, --no-source-map, and --load-path. Add load paths narrowly; a huge search path makes module origins harder to reason about. The fuller setup guide is how to install and compile Sass.

Your first SCSS file

Create scss/main.scss:

$brand: #5b3fd6;
$space: 1rem;

.notice {
  padding: $space $space * 1.5;
  border-inline-start: 0.25rem solid $brand;

  &:hover {
    background: #f5f2ff;
  }
}

Sass replaces the variables, performs the multiplication, expands the nesting, and emits CSS. $brand and $space do not exist in the browser.

That is the first important boundary:

Need Prefer
Fixed build-time token or math Sass variable $space
Value that changes by element or theme CSS custom property --color
Generate repeated static rules Sass loop or mixin
Let the cascade choose at runtime Native CSS

Sass variables are imperative and compiled away, so their names add no bytes to output CSS. They support build-time arithmetic—use normal + and *, and math.div() for division. CSS custom properties stay in the output and add bytes, but inherit and vary at runtime; runtime arithmetic belongs in calc(). Modern projects often use both:

$brand: #5b3fd6;

:root {
  --brand: #{$brand};
}

.button {
  background: var(--brand);
}

The #{} syntax is interpolation: it inserts a Sass value where Sass would otherwise pass custom-property text through unchanged. For a broader runtime comparison, see CSS custom properties.

Nest for relationships, not to photocopy the DOM

SCSS nesting is helpful for states, pseudo-elements, and tightly related descendants:

.card {
  padding: 1rem;

  &:hover {
    box-shadow: 0 0.5rem 1.5rem rgb(0 0 0 / 12%);
  }

  &__title {
    margin-block: 0 0.5rem;
  }
}

The parent selector & compiles &:hover to .card:hover and &__title to .card__title.

Do not nest merely because HTML elements sit inside one another. A four-level SCSS doll produces a four-level CSS selector with extra specificity and structural coupling. Keep selectors understandable in the generated CSS. Native CSS nesting now overlaps with this Sass feature, but syntax and edge cases are not identical; native CSS nesting versus Sass nesting covers the migration question.

Modules: use @use, expose with @forward

New Sass should use the module system. @use loads a stylesheet as a module, includes its CSS once, and gives public variables, mixins, and functions a namespace.

// scss/tokens/_colors.scss
$brand: #5b3fd6;
$ink: #201a2b;
// scss/components/_button.scss
@use "../tokens/colors";

.button {
  color: white;
  background: colors.$brand;
}
// scss/main.scss
@use "components/button";

You may shorten or remove a namespace:

@use "tokens/colors" as c; // c.$brand
@use "tokens/spacing" as *; // $space-sm; use sparingly

as * can cause collisions, so reserve it for modules you control. A file beginning with _ is a partial and is not emitted as a separate CSS file when a directory is compiled. You omit the underscore and extension in the module URL.

@use rules must appear before style rules, although configurable variable declarations can precede them. Members loaded in one file do not magically appear in another; each file declares what it uses. That explicitness feels fussy for nine minutes and helpful for the following nine years.

Use @forward to create one public entrypoint:

// scss/tokens/_index.scss
@forward "colors";
@forward "spacing";
// scss/components/_button.scss
// _spacing.scss defines $space-sm and $space-md.
@use "../tokens";

.button {
  padding: tokens.$space-sm tokens.$space-md;
  background: tokens.$brand;
}

@forward exposes members to downstream users; it does not automatically make them available inside the forwarding file. If a file both forwards and uses the same module, the official docs recommend placing @forward first so downstream configuration is applied before the local @use.

Configure a module with !default and with

Libraries can publish overridable defaults:

// scss/theme/_index.scss
$radius: 0.5rem !default;
$brand: #5b3fd6 !default;

.theme-button {
  border-radius: $radius;
  background: $brand;
}

Configure the module the first time it is loaded:

// scss/main.scss
@use "theme" with (
  $radius: 999px,
  $brand: #006b5f
);

Configuration is a module API, so expose only variables you intend consumers to set. Since Dart Sass 1.92.0, configuring a private variable emits a deprecation warning. A leading - or _ marks a member private; do not build a package that relies on reaching through that boundary with a with block.

Mixins output styles; functions return values

A mixin packages declarations or rules:

@mixin focus-ring($color) {
  outline: 0.2rem solid $color;
  outline-offset: 0.2rem;
}

.button:focus-visible {
  @include focus-ring(currentColor);
}

A function computes a Sass value:

@use "sass:math";

@function rem($pixels, $base: 16) {
  @return math.div($pixels, $base) * 1rem;
}

.title {
  font-size: rem(24);
}

Use math.div() rather than slash division. Dart Sass 1.33.0 introduced math.div() and the deprecation warning for / division because CSS uses slash as a separator in valid syntax. Saying slash division has already vanished everywhere is inaccurate; current 1.x can still warn for legacy cases. New code should not depend on that transition period.

Mixins can quietly duplicate lots of CSS when included repeatedly. Inspect the compiled output. The source may be magnificently DRY while the delivered file is wearing six identical coats. Our mixins versus functions guide gives a fuller decision tree.

Conditionals, errors, and warnings

Use @if, @else if, and @else when a mixin or function must choose among valid paths:

@mixin button-variant($style: solid) {
  @if $style == solid {
    background: currentColor;
  } @else if $style == outline {
    border: 2px solid currentColor;
  } @else {
    @error "Unknown button style: #{$style}";
  }
}

@error prints a message and stack trace, stops compilation, and prevents silently wrong CSS. Use @warn for a non-fatal problem or deprecation in your own API; it reports the warning without stopping the whole build.

Built-in modules and the color-function migration

Load built-ins from their sass: modules:

@use "sass:color";
@use "sass:map";
@use "sass:math";

Then call functions through the namespace:

@use "sass:color";

$brand: #5b3fd6;

.button:hover {
  background: color.adjust(
    $brand,
    $lightness: -10%,
    $space: hsl
  );
}

This corrects two common stale-tutorial patterns. First, global versions of built-ins that live in modules have been deprecated since Dart Sass 1.80.0. Second, legacy single-channel color helpers including darken() and lighten() are deprecated as Sass adapts to CSS Color 4. Use current sass:color APIs and make the color space intentional where the operation requires it.

Do not mechanically replace darken($x, 10%) with whichever new name looks adjacent. color.adjust() and color.scale() have different semantics. Compare the rendered result and contrast in every supported theme.

Maps and loops: generate carefully

Maps are useful for build-time token sets:

@use "sass:map";

$spaces: (
  "sm": 0.5rem,
  "md": 1rem,
  "lg": 2rem
);

.stack {
  gap: map.get($spaces, "md");
}

@each $name, $value in $spaces {
  .gap-#{$name} {
    gap: $value;
  }
}

The loop produces three classes. That can be useful; a map with forty properties across six breakpoints can produce a small CSS weather event. Measure generated output and only emit utilities the project consumes. For nested maps, keep access paths named and test the generated design-token output.

A project structure that can grow without ceremony

Many Sass tutorials present the “7-1 pattern” as though every serious team graduates into exactly seven folders and one output file. It is one convention, not a Sass requirement. Start smaller:

scss/
├── tokens/
│   ├── _colors.scss
│   ├── _spacing.scss
│   └── _index.scss
├── base/
│   ├── _reset.scss
│   └── _type.scss
├── components/
│   ├── _button.scss
│   └── _card.scss
└── main.scss

Add layout, pages, utilities, themes, or vendor boundaries only when the codebase needs them. More folders do not make CSS scalable by osmosis. Clear ownership, limited output, stable module APIs, and predictable entrypoints do.

Whether generated CSS belongs in Git is also a project decision. If deployment compiles assets, ignoring output can prevent stale artifacts. If a host or consumer expects committed CSS, omitting it breaks delivery. Document one rule in the repository and have CI verify the build.

Migrate old Sass without rewriting it blindfolded

Sass @import and global built-ins have emitted deprecation warnings since Dart Sass 1.80.0. Official docs say removal is planned for Dart Sass 3.0.0. In the current 1.x release, deprecated imports may still compile; describe them as deprecated, not already removed.

For a real migration:

  1. Commit or otherwise preserve a clean baseline.
  2. Pin the compiler version and run the existing build and visual tests.
  3. Run the official Sass migrator on an entrypoint with dependency migration enabled.
  4. Review namespaces and generated CSS rather than accepting every automatic name.
  5. Replace legacy global built-ins and color functions deliberately.
  6. Upgrade in small steps and treat new deprecation warnings as work items.

Run the official migrator on a branch and consult its current command reference for the exact entrypoint and dependency flags. If build tooling calls Sass through JavaScript, also check for the deprecated legacy render() or renderSync() API; the modern API uses compile(), compileAsync(), compileString(), and compileStringAsync().

Make compiler warnings fail somewhere useful

A warning noticed in local development is helpful. The same warning repeated for six months in a noisy build log is decorative wallpaper. Capture the compiler version in the lockfile, keep one reproducible build command, and run it in continuous integration. Sass exposes CLI controls for deprecations, including options to make selected deprecations fatal, but add them deliberately: a dependency you do not control may warn before your own code does.

Start by running the build with normal warnings and recording which messages come from first-party files and which come from dependencies. Fix your code. Update or replace dependencies through their documented releases. Only silence a dependency warning temporarily when you have an owner and removal date; otherwise “temporary” settles in, receives mail, and becomes infrastructure.

Source maps deserve the same explicit policy. They are valuable during development because browser tools can point from generated CSS back to the relevant SCSS module. Production handling depends on whether maps are deployed publicly, uploaded privately to an error service, or omitted. The Dart Sass CLI offers --no-source-map and source-map URL controls. Pick a policy based on deployment rather than deleting every .map file because an old blog called them clutter. If mappings or generated rules look stale, delete the project's generated dist/ directory and recompile from a clean source tree; never delete an unverified path.

Finally, compare output across compiler upgrades. A clean compile is necessary, but not sufficient: snapshot important generated CSS, run visual or component tests, and scan bundle size. Preprocessor migrations are unusually good at producing valid CSS that is nevertheless different CSS—the most polite kind of regression.

Is Sass still worth using?

Use Sass when its build-time features pay rent: configurable modules, code generation from maps, reusable mixins, custom functions, or a library API. Native CSS now covers custom properties and nesting, so a small site that wants only those features may be simpler without a preprocessing step.

The decision is not ideological. Compile a representative entrypoint and inspect the output. Consider browser requirements, team familiarity, warning maintenance, source maps, build time, and whether runtime theming belongs in CSS. Sass remains useful; it no longer needs to be invited to every project merely because the invitation template says “front end.” Our honest take lives in the Sass & SCSS hub.

Quick reference

Feature SCSS syntax What reaches CSS
Variable $space: 1rem Value only
Module @use "tokens" Module CSS, once
Mixin @include focus-ring Declarations or rules
Function rem(24) Returned value
Conditional @if ... @else Chosen branch only
Loop @each Generated rules
Custom property --space: #{$space} Variable and value

Modern Sass checklist

The browser never sees how clever your SCSS was. It sees CSS. Keep that output boring, correct, and small; Sass can do the interesting work backstage.

Sources checked

FAQ

How do I install Sass in a Node project?

Run `npm install --save-dev sass`, then invoke the project-local executable with an npm script or `npx sass`. A useful script is `sass scss/main.scss public/main.css`, with a second script adding `--watch` for development. Run `npx sass --version` to record what the project actually uses. The official Sass site also offers `sass-embedded`, standalone archives, and system-package options; choose one implementation deliberately rather than mixing global and local versions.

Should new Sass projects use @use or @import?

Use `@use` for modules and `@forward` to expose a curated public entrypoint. Sass `@import` has emitted deprecation warnings since Dart Sass 1.80.0 and is planned for removal in Dart Sass 3.0.0. It may still compile in the current 1.x release, so “deprecated” is not the same as “already removed.” Use the official Sass migrator and tests for an old codebase instead of performing a blind search-and-replace.

What is the difference between Sass variables and CSS custom properties?

A Sass variable such as `$space` is evaluated during compilation and disappears into the generated CSS. A CSS custom property such as `--space` remains in the browser, participates in the cascade, and can change by element, media query, theme, or JavaScript. Use Sass for build-time configuration and code generation; use custom properties for runtime values. To put a Sass value into a custom property, interpolate it: `--brand: #{$brand};`.

Should I use a Sass mixin or function?

Use a mixin when the reusable result is a group of declarations or rules; call it with `@include`. Use a function when the result is one Sass value used inside a declaration; return it with `@return`. Keep both small and named for intent. If a mixin emits a large block in many selectors, inspect the compiled CSS because reuse in source can still duplicate output. The browser downloads the result, not the elegance of your SCSS.

How should I divide numbers and adjust colors in modern Sass?

Load built-in modules explicitly. Use `@use 'sass:math'` with `math.div()` instead of slash division. Use `@use 'sass:color'` with Color 4-aware functions such as `color.adjust()` or `color.scale()` and state the color space where appropriate. Legacy helpers including `darken()` and `lighten()` are deprecated, as are global calls to built-ins that now live in modules. Read compiler warnings before they become upgrade-week fireworks.

Is Sass still useful now that CSS has nesting and custom properties?

Yes when a project benefits from build-time modules, configurable libraries, maps, loops, reusable mixins, or functions that generate static CSS. It may be unnecessary if the project only wanted variables and simple nesting, because modern CSS supplies runtime custom properties and native nesting. Choose by output and maintenance cost: compare the generated CSS, browser requirements, team familiarity, and tooling burden. Sass is a tool, not a mandatory front-end initiation ceremony.