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 is a CSS preprocessor you write in an enhanced syntax with variables, nesting, mixins, and functions, then compile to plain CSS. To start, install Dart Sass with `npm install --save-dev sass`, write partials in `.scss` files, pull them together with `@use`, and run `sass scss/main.scss dist/main.css --watch` to compile automatically as you save.

Sass in one sentence

Sass is a CSS preprocessor: you write stylesheets in an enhanced syntax with variables, nesting, mixins, and functions, then compile them down to plain CSS the browser can read. This guide takes you from a first install to the file structure large teams use in production, using the modern module system (@use / @forward) rather than the deprecated @import you'll still see in older tutorials.

We'll work in SCSS, the syntax that looks like CSS with superpowers. If you're still deciding between the two syntaxes, read Sass vs SCSS first; if you've never touched a preprocessor, What Is Sass? is the gentler starting point. Everything below assumes you know basic CSS.

Get Sass running in two minutes

Sass is distributed as Dart Sass, the single official implementation since 2019. (The old Ruby Sass and LibSass/node-sass projects are both deprecated and should not be used on new projects.) The fastest way to install it on a Node project:

npm install --save-dev sass

Then add a compile script to package.json:

{
  "scripts": {
    "css": "sass scss/main.scss dist/main.css",
    "watch": "sass --watch scss/main.scss dist/main.css"
  }
}

Run npm run watch and Sass recompiles every time you save. For a global CLI, a standalone binary, or a build-tool plugin (Vite, webpack), see our full walkthrough on how to install Sass. The commands below assume the scss/main.scssdist/main.css layout above.

Compile options worth knowing

Flag What it does
--watch Recompile on every file save
--style=compressed Minify output for production
--no-source-map Skip the .map file (on by default)
--load-path=node_modules Add a directory to the module search path
--update Only compile files that changed

A typical production build is just sass scss/main.scss dist/main.css --style=compressed --no-source-map.

Variables: name your values once

Sass variables start with $ and hold any CSS value — colors, sizes, fonts, even lists and maps. Define once, reuse everywhere, change in one place.

$brand: #2f6feb;
$radius: 8px;
$space: 1rem;

.button {
  background: $brand;
  border-radius: $radius;
  padding: $space ($space * 1.5);
}

Note the math: Sass evaluates $space * 1.5 at compile time, so the CSS ships as padding: 1rem 1.5rem. That arithmetic is the main thing Sass variables still do that native CSS custom properties can't.

Sass variables vs CSS custom properties

They solve overlapping problems, and modern codebases often use both.

Sass $variable CSS --variable
Resolved At compile time In the browser, at runtime
Supports math (* / +) Yes Only via calc()
Changes with media query / JS No Yes
Themable live (dark mode toggle) No Yes
Adds bytes to output CSS No Yes

Rule of thumb: use Sass variables for values fixed at build time (breakpoints, spacing scales, math) and CSS custom properties for anything that changes in the browser, like theme colors. You can even hand a Sass value to a custom property: :root { --brand: #{$brand}; }. The #{} is interpolation — it drops a Sass value into a context where Sass wouldn't otherwise evaluate it.

Nesting: structure that mirrors your markup

Nesting lets you write child selectors inside their parent, so related rules live together.

.card {
  padding: 1.5rem;
  border-radius: $radius;

  .card__title {
    margin: 0 0 0.5rem;
    font-size: 1.25rem;
  }

  &:hover {
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
  }

  &--featured {
    border: 2px solid $brand;
  }
}

The & is the parent selector. &:hover compiles to .card:hover, and &--featured compiles to .card--featured — that second pattern is how you write BEM modifiers without repeating the block name.

The one nesting mistake to avoid: don't nest to mirror your HTML tree three or four levels deep. Every level of nesting increases specificity and produces selectors like .header .nav .list .item a that are brittle and hard to override. Keep nesting one or two levels, and lean on & for states and modifiers rather than deep descendant chains. Native CSS now supports nesting too, so this restraint applies either way.

Partials and the module system

Real projects split styles across many files. In Sass, a file whose name starts with an underscore — _buttons.scss — is a partial: it won't compile to its own CSS file, only when pulled into another file.

The modern way to pull them in is @use, which replaced @import (officially deprecated and scheduled for removal). @use loads a file once, no matter how many times it's referenced, and namespaces its members so nothing leaks globally.

// scss/main.scss
@use 'variables';
@use 'buttons';
@use 'card';

By default, members are namespaced by filename:

// _variables.scss
$brand: #2f6feb;

// _buttons.scss
@use 'variables';

.button { background: variables.$brand; }

You can shorten or drop the namespace when it reads better:

@use 'variables' as v;      // v.$brand
@use 'variables' as *;      // $brand, no prefix (use sparingly)

Why @use beats @import

@use (modern) @import (deprecated)
Loads a file more than once No — loaded once Yes — duplicated
Variable scope Namespaced, private-able All global
Name collisions Prevented by namespace Silent overwrites
Status Recommended Being removed from Sass

If you're starting today, use @use and @forward exclusively. @forward re-exports a partial's members so you can build an "index" file that bundles several partials behind one entry point — the backbone of the folder structure below.

Mixins and functions

These are where Sass earns its keep on a large codebase.

A mixin (@mixin / @include) stamps out a reusable block of declarations, optionally parameterized:

@mixin flex-center($direction: row) {
  display: flex;
  flex-direction: $direction;
  align-items: center;
  justify-content: center;
}

.hero {
  @include flex-center(column);
}

A function (@function / @return) computes and returns a single value you use inside a declaration:

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

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

Note math.div() — Sass removed the / division operator because / is ambiguous with the CSS shorthand slash (as in font: 16px/1.5). Always divide with math.div() from the built-in sass:math module, which you load with @use 'sass:math'; at the top of the file.

The short version: mixins output declarations, functions return values. For a deeper decision framework with real examples, see Sass mixins vs functions.

Built-in modules

Modern Sass ships its helpers as modules you @use explicitly rather than global functions:

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

.link {
  color: $brand;

  &:hover {
    // darken() is deprecated; color.adjust is the current API
    color: color.adjust($brand, $lightness: -12%);
  }
}

The old global darken(), lighten(), and percentage() functions still work for now but are being phased out in favor of color.adjust(), color.scale(), and the math module. Reach for the module version in new code.

Loops, conditionals, and maps

Sass has real control flow, which shines when generating repetitive utility classes or reading design tokens from a map.

A map is a set of key–value pairs — perfect for a spacing or color scale:

@use 'sass:map';

$spacers: (
  'sm': 0.5rem,
  'md': 1rem,
  'lg': 2rem,
);

@each $name, $size in $spacers {
  .mt-#{$name} { margin-top: $size; }
  .mb-#{$name} { margin-bottom: $size; }
}

That @each loop generates six utility classes from three map entries. Conditionals use @if / @else:

@mixin button-variant($style: 'solid') {
  @if $style == 'solid' {
    background: $brand;
    color: white;
  } @else if $style == 'outline' {
    background: transparent;
    border: 2px solid $brand;
  } @else {
    @error "Unknown button style: #{$style}.";
  }
}

@error halts compilation with a clear message — far better than shipping silently wrong CSS. There's also @warn for non-fatal deprecation notices in your own mixins.

Structuring a real project: the 7-1 pattern

Once you're past a single file, you need a folder convention. The widely used 7-1 pattern organizes partials into folders, all bundled by one main.scss:

scss/
├── abstracts/    // variables, mixins, functions (no CSS output)
│   ├── _variables.scss
│   └── _mixins.scss
├── base/         // resets, typography, base element styles
├── components/   // buttons, cards, forms
├── layout/       // header, footer, grid
├── pages/        // page-specific styles
├── themes/       // dark mode, high contrast
├── vendors/      // third-party CSS
└── main.scss     // @use everything, the only file that compiles

The name is "7 folders, 1 output file." Small projects rarely need all seven — start with abstracts, base, components, and layout, and add folders only when a category actually earns its own directory. The point is a predictable home for every rule, not ceremony.

Your main.scss stays thin:

@use 'abstracts/variables';
@use 'abstracts/mixins';
@use 'base/reset';
@use 'base/typography';
@use 'components/button';
@use 'layout/header';

Sass in 2026: is it still worth learning?

Native CSS has absorbed several of Sass's headline features — nesting, custom properties, and @layer for cascade control all ship in every current browser. That's led to a fair question: do you still need a preprocessor at all? Our honest take lives in the Sass & SCSS hub, but the short answer is that Sass still offers things plain CSS doesn't: compile-time loops, maps, functions with real logic, @each-generated utilities, and @use-based file organization with private members. If your project leans on those, Sass remains a strong pick. If you only wanted variables and nesting, native CSS may now be enough.

Quick reference

Feature Syntax Compiles to
Variable $brand: #2f6feb; Inlined value
Nesting .a { .b { … } } .a .b { … }
Parent selector &:hover .a:hover
Mixin @include flex-center; The mixin's declarations
Function rem(24) A returned value (1.5rem)
Partial import @use 'buttons'; Merged, loaded once
Loop @each $k, $v in $map Repeated rules

Five mistakes beginners make with Sass

Most Sass frustration traces back to the same handful of habits. Fixing them early saves hours of confusing output.

Debugging tip: read your source maps

Sass emits a .map file by default. When your browser's dev tools show a rule as coming from main.css:812, the source map lets it point instead at the exact partial and line — _button.scss:14. Keep source maps on during development and strip them (--no-source-map) for production builds. If the mapping ever looks wrong, delete the dist/ folder and recompile from scratch to clear stale output.

Where to go next

You now have the whole toolkit: install and compile, variables and math, nesting with &, partials via @use/@forward, mixins versus functions, control flow with maps, and a folder structure that scales. The best way to lock it in is to convert one real stylesheet you already have — move its magic numbers into variables, pull repeated blocks into mixins, and split it into partials. You'll feel where Sass helps, and where plain CSS was already fine.

From here, the natural next steps are getting your build airtight in how to install Sass and sharpening the mixin-vs-function instinct in Sass mixins vs functions.

FAQ

How do I install Sass?

Install the official Dart Sass implementation. On a Node project, run `npm install --save-dev sass`, then compile with `sass scss/main.scss dist/main.css`. Add `--watch` to recompile on every save. You can also install a global CLI or a standalone binary. Avoid the deprecated Ruby Sass and node-sass packages on any new project.

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

Sass variables (`$brand`) resolve at compile time and support math, so they're ideal for breakpoints and spacing scales, but they can't change in the browser. CSS custom properties (`--brand`) resolve at runtime, so they power live theming like dark mode. Modern codebases use both, handing Sass values to custom properties with interpolation: `--brand: #{$brand};`.

Should I use @use or @import in Sass?

Use `@use` (and `@forward`). The older `@import` is officially deprecated and being removed: it loads files multiple times and leaks every variable into a global namespace. `@use` loads each file once and namespaces its members, preventing accidental name collisions. A `sass-migrator` tool can convert an existing `@import`-based codebase automatically.

What is the difference between a Sass mixin and a function?

A mixin (`@mixin`/`@include`) outputs a block of CSS declarations and is used for reusable groups of styles. A function (`@function`/`@return`) computes and returns a single value used inside a declaration, like a converted rem size. Short version: mixins output declarations, functions return values. Choose a function when you only need one computed value.

Is Sass still worth learning in 2026?

Yes, if your project needs what plain CSS still lacks. Native CSS now has nesting, custom properties, and cascade layers, covering simple needs. But Sass still offers compile-time loops, maps, real functions with logic, `@each`-generated utility classes, and `@use`-based file organization with private members. For large, token-driven codebases those features remain genuinely useful.

How do I divide numbers in Sass?

Use the `math.div()` function, not the `/` operator. Sass removed slash-as-division because `/` is ambiguous with CSS shorthand like `font: 16px/1.5`. Load the math module once at the top of your file with `@use 'sass:math';`, then write `math.div($px, 16)`. The old `/` syntax now emits a deprecation warning and will be removed.