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

- Sass in one sentence
- Get Sass running in two minutes
- Variables: name your values once
- Nesting: structure that mirrors your markup
- Partials and the module system
- Mixins and functions
- Loops, conditionals, and maps
- Structuring a real project: the 7-1 pattern
- Sass in 2026: is it still worth learning?
- Five mistakes beginners make with Sass
- Where to go next
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.scss → dist/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.
- Over-nesting. Deeply nested selectors bloat specificity and generate output like
.page .sidebar .widget ul li a. Keep nesting to one or two levels and use&for states. - Still using
@import. It's deprecated, loads files multiple times, and dumps everything into a global namespace. Migrate to@useand@forward— the tooling even ships asass-migratorto help. - Dividing with
/. In current Sass,width: $a / $bno longer divides. Usemath.div($a, $b)after@use 'sass:math';. - Reaching for Sass variables where custom properties belong. A Sass
$variableis frozen at compile time, so it can't power a runtime dark-mode toggle. Use--variablesfor anything the browser needs to change. - Committing compiled CSS. Add
dist/(or wherever your CSS lands) to.gitignoreand compile as part of your build. Checking in generated CSS leads to merge conflicts and stale files.
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.