The Complete Guide to Sass

Introduction to Sass

Sass (Syntactically Awesome Style Sheets) is a CSS preprocessor that adds variables, nesting, mixins, functions, modules, and control directives to plain CSS. It compiles down to standard, browser-readable CSS, letting you write more maintainable, modular, and reusable stylesheets.

Information

Sass was created by Hampton Catlin and developed by Natalie Weizenbaum, first released in 2006. The current reference implementation, Dart Sass, is maintained by the Sass core team.

Why Use a CSS Preprocessor?

Plain CSS lacks basic programming constructs. As stylesheets grow, repetition, poor organization, and lack of abstraction become real problems. Sass addresses this directly.

  • Variables — store reusable values like colors, fonts, and spacing.
  • Nesting — mirror your HTML structure in your styles.
  • Partials & Modules — split CSS into small, manageable files.
  • Mixins & Functions — write reusable, DRY style logic.
  • Control directives like @if, @each, @for, and @while.
  • A rich set of built-in modules for math, color, string, list, and map manipulation.

Sass vs SCSS Syntax

Sass ships with two interchangeable syntaxes: the original indented Sass syntax, and SCSS (Sassy CSS), a strict superset of CSS. SCSS is by far the more widely used syntax today.

style.scss

.card {
  padding: 1rem;

  &__title {
    font-weight: bold;
  }
}

style.sass

.card
  padding: 1rem

  &__title
    font-weight: bold
FeatureSCSSSass (Indented)
File extension.scss.sass
Braces & semicolonsRequiredNot used
Whitespace significanceNoYes
Comments// and /* */// and /*
AdoptionMost commonRare

Tip

You can convert between the two syntaxes losslessly using the sass-convert tool bundled with Ruby Sass, or the sass --convert option in some CLI tooling.

Comments in Sass

Sass supports two comment styles. Which one you use determines whether the comment survives compilation into the final CSS output.

Silent comments use // and are stripped entirely from the compiled CSS — they exist only in the source .scss file.

silent-comments.scss

// This line explains the next rule
.btn {
  // TODO: replace with design token
  padding: 8px 16px;
}

Loud comments use /* */ and are preserved in the compiled output by default, making them useful for license headers or notes meant for the final CSS file.

loud-comments.scss

/*!
 * MyLibrary v1.0.0
 * Preserved even in compressed output
 */
.card {
  /* Base card styling */
  border-radius: 4px;
}
Comments and Interpolation

Loud comments can contain # interpolation, letting you embed dynamic Sass values directly into the compiled comment text.

interpolated-comment.scss

$version: '2.3.1';

/* Compiled on version: #{$version} */
.app { color: $primary-color; }
Comments and Compressed Output
Comment TypeExpanded OutputCompressed Output
// silentRemovedRemoved
/* */ loudKeptRemoved, unless it starts with /*!

Tip

Use /*! ... */ for content you want to survive even in --style=compressed production builds — such as copyright or license banners.

Best Practice

Prefer // for everyday developer notes and TODOs, and reserve /* */ for comments that genuinely belong in the shipped CSS.

Sass vs LESS vs Stylus

FeatureSassLESSStylus
RuntimeDart / standalone binaryJavaScriptJavaScript
Module system@use/@forwardBasic @importBasic @import
Control flow@if, @each, @for, @whileGuards / mixinsConditionals
MaturityMost mature ecosystemMatureSmaller community

Installation & Tooling Setup

The modern reference implementation is Dart Sass, distributed via npm, as a standalone executable, or embedded in build tools. LibSass and node-sass are deprecated and should not be used in new projects.

terminal

npm install -D sass

terminal

yarn add -D sass

terminal

pnpm add -D sass

terminal

sass --version

Compiling Sass to CSS

terminal

# Compile once
sass input.scss output.css

# Watch for changes
sass --watch input.scss:output.css

# Watch an entire directory
sass --watch styles:dist

# Compressed production output
sass --style=compressed input.scss output.min.css

# Generate source maps (on by default)
sass --source-map input.scss output.css

Tip

Use --watch during development so .scss files recompile automatically, and use --style=compressed for production builds to minimize file size.

How Compilation Works

.scss Source Files
Variables Resolved
Nesting Flattened
Sass Compiler
Mixins & Functions Expanded
Plain .css Output
Dart Sass Engine

Integrating Sass with Build Tools

Vite has built-in Sass support — simply install the sass package and import .scss files directly.

terminal

npm install -D sass

main.js

import './styles/main.scss';

Webpack requires sass-loader, css-loader, and style-loader (or MiniCssExtractPlugin for production).

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.scss$/,
        use: ['style-loader', 'css-loader', 'sass-loader'],
      },
    ],
  },
};

gulpfile.js

const gulp = require('gulp');
const sass = require('gulp-sass')(require('sass'));

gulp.task('styles', () =>
  gulp.src('src/scss/**/*.scss')
    .pipe(sass().on('error', sass.logError))
    .pipe(gulp.dest('dist/css'))
);

Using the VS Code Extension to Compile SASS/SCSS

Instead of using the command-line compiler, many developers prefer a VS Code extension called Live Sass Compiler, which watches .scss/.sass files and compiles them to CSS automatically on save, right from the editor.

Installing the Extension

  1. Open VS Code and go to the Extensions panel (Ctrl+Shift+X on Windows/Linux, Cmd+Shift+X on macOS).
  2. Search for Live Sass Compiler by Glenn Marks.
  3. Click Install.
  4. Reload the editor if prompted.

Compiling with One Click

Once installed, open any .scss file in your workspace and click Watch Sass in the VS Code status bar at the bottom of the window.

Open .scss File
Click "Watch Sass" in Status Bar
Extension Watches for Changes
Auto-Generated .css + .css.map
Save the File

Tip

Once watching is active, the status bar label changes to Watching..., and every save instantly regenerates the compiled CSS file and its .map source map alongside it.

Configuring Output Behavior

The extension's behavior can be customized via settings.json, including output folder, formatting style, and which files to exclude.

.vscode/settings.json

{
  "liveSassCompile.settings.formats": [
    {
      "format": "expanded",
      "extensionName": ".css",
      "savePath": "/dist/css"
    }
  ],
  "liveSassCompile.settings.generateMap": true,
  "liveSassCompile.settings.excludeList": [
    "**/node_modules/**",
    ".vscode/**"
  ]
}

Available Output Formats

FormatDescription
expandedFully readable, multi-line CSS — good for development.
compressedMinified, single-line CSS — ideal for production.

CLI vs Extension

AspectDart Sass CLILive Sass Compiler Extension
SetupRequires npm/global installPoint-and-click install from marketplace
Best forProduction builds, CI pipelines, build tool integrationQuick projects, learning, static sites without a build step
ConfigurationCLI flags / build tool configsettings.json
Module system supportFull @use/@forward supportFull support (uses Dart Sass under the hood)

Note

The Live Sass Compiler extension uses Dart Sass internally, so all the same syntax, modules, and features covered earlier in this tutorial apply identically — only the compilation workflow differs.

Variables

Variables in Sass are prefixed with $ and can store colors, numbers, strings, booleans, lists, maps, and even null.

_variables.scss

$primary-color: #3498db;
$spacing-unit: 8px;
$font-stack: 'Helvetica Neue', Arial, sans-serif;
$is-rounded: true;

.button {
  background-color: $primary-color;
  padding: $spacing-unit * 2;
  font-family: $font-stack;
}

Default Values with !default

The !default flag assigns a value only if the variable isn't already defined. This is essential for building configurable, shareable libraries.

_config.scss

$border-radius: 4px !default;
$enable-shadows: true !default;

// A consumer can override before loading the module:
// $border-radius: 8px;
// @use 'config';

Global Variables with !global

By default, variables assigned inside a block (like a mixin or rule) are locally scoped. !global forces an assignment to affect the global scope.

scope.scss

$counter: 0;

@mixin increment {
  $counter: $counter + 1 !global;
}

.a { @include increment; }
.b { @include increment; }
// $counter is now 2 globally

Warning

Overusing !global makes stylesheets harder to reason about. Prefer returning values from @function where possible instead of mutating global state.

Variable Scope Rules

  1. Variables declared at the top level of a stylesheet are global.
  2. Variables declared inside a rule, mixin, or function are local to that block.
  3. A local variable with the same name as a global one will shadow it within that block only.
  4. Control-flow blocks (@if, @each, @for, @while) share scope with their surrounding block, not a new one.

Nesting

Nesting lets selectors mirror your HTML structure, reducing repetition and keeping related styles together.

nesting.scss

.nav {
  display: flex;

  &__item {
    margin-right: 1rem;

    &:hover {
      color: $primary-color;
    }

    &--active {
      font-weight: bold;
    }
  }
}

Warning

Avoid nesting more than 3–4 levels deep — it produces overly specific selectors, bloated output CSS, and harder-to-override rules.

The Parent Selector &

The ampersand refers to the parent selector and enables BEM-style naming, pseudo-classes, pseudo-elements, and combined/compound selectors.

ampersand.scss

.btn {
  &.is-disabled { opacity: 0.5; }
  &::before { content: ''; }
  .dark-theme & { background: black; }
  & + & { margin-left: 8px; }
}

Nesting Properties

Sass also allows nesting of properties that share a common namespace, such as font or border.

nested-properties.scss

.text {
  font: {
    family: $font-stack;
    size: 16px;
    weight: bold;
  }
}

Nested Media Queries

nested-media.scss

.container {
  width: 100%;

  @media (min-width: 768px) {
    width: 750px;
  }
}

Partials, @use, and @forward

Large stylesheets are split into partials — files prefixed with an underscore, like _buttons.scss — which are never compiled to standalone CSS files on their own.

Project File Structure

styles
main.scss
base
_reset.scss
_typography.scss

@use — The Modern Import

@use loads a module exactly once per compilation and namespaces its members by default, avoiding the global-scope pollution that plagued the legacy @import rule.

main.scss

@use 'utils/variables' as vars;
@use 'utils/mixins';

.card {
  color: vars.$primary-color;
  @include mixins.rounded-corners;
}

Customizing the Namespace

namespace.scss

// Default namespace is the file name: 'variables'
@use 'utils/variables';

// Custom namespace
@use 'utils/variables' as vars;

// No namespace — access members directly (use sparingly)
@use 'utils/variables' as *;

Configuring Modules with with

Modules that declare !default variables can be configured at load time using @use ... with (...), without editing the module itself.

configure.scss

// _library.scss
$primary: blue !default;
$radius: 4px !default;

// consumer.scss
@use 'library' with (
  $primary: teal,
  $radius: 8px
);

@forward — Re-exporting Modules

@forward makes another module's members available to anyone who loads your module, which is how index/entry-point partials are built.

utils/_index.scss

@forward 'variables';
@forward 'mixins';
@forward 'functions' show rem, strip-unit;

Private Members

Any variable, mixin, or function whose name begins with - or _ is treated as private to its module and won't be exposed by @use or @forward.

private.scss

$-internal-cache: (); // private
$public-value: 10px;  // public

Important

@import is officially deprecated in Dart Sass and will eventually be removed. New projects should always prefer @use and @forward.

Migrating @import to @use/@forward

Converting a large, legacy codebase from @import to the module system by hand is error-prone. The Sass team maintains an official CLI tool, sass-migrator, that automates this process.

Installing the Migrator

terminal

npm install -g sass-migrator
Running the Migration

terminal

# Migrate a single entry-point file and everything it imports
sass-migrator module --migrate-deps main.scss

# Preview changes without writing to disk
sass-migrator module --dry-run --migrate-deps main.scss

Tip

The --migrate-deps flag is essential — without it, only the entry file is converted, leaving every partial it imports still using the old @import syntax.
What the Migrator Changes
  • Rewrites @import 'partial' statements into @use 'partial' as name.
  • Prefixes every migrated variable, mixin, and function call with its correct module namespace.
  • Resolves naming collisions automatically by assigning distinct namespaces per file.
  • Leaves files untouched if they contain constructs the migrator cannot safely convert, flagging them instead.
Before and After

main.scss (legacy)

@import 'variables';
@import 'mixins';

.card {
  color: $primary-color;
  @include rounded-corners;
}

main.scss (migrated)

@use 'variables';
@use 'mixins';

.card {
  color: variables.$primary-color;
  @include mixins.rounded-corners;
}
Migrating a Library's Public API

For a shared library meant to be consumed by others, use the --forward=all option so the migrator also generates @forward statements, preserving the library's public surface.

terminal

sass-migrator module --migrate-deps --forward=all index.scss
Post-Migration Checklist
  1. Re-run your full build and visually diff the compiled CSS output against the pre-migration version.
  2. Search for any remaining @import statements the tool flagged as unsafe to auto-convert.
  3. Check for duplicate namespace conflicts, especially in deeply nested partial structures.
  4. Remove now-unused global variable duplication that @import previously allowed.

Warning

Always commit your working tree before running sass-migrator without --dry-run — it rewrites files in place.

Mixins

@mixin defines a reusable, named block of styles, optionally parameterized with arguments, and invoked with @include.

mixins.scss

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

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

Keyword & Default Arguments

keyword-args.scss

@mixin button($bg: blue, $color: white, $radius: 4px) {
  background: $bg;
  color: $color;
  border-radius: $radius;
}

.btn-danger {
  @include button($bg: red, $radius: 8px);
}

Variable Arguments (...)

The ... syntax lets a mixin or function accept an arbitrary number of arguments, collected into a list.

variadic.scss

@mixin box-shadow($shadows...) {
  box-shadow: $shadows;
}

.card {
  @include box-shadow(0 1px 2px rgba(black, 0.1), 0 4px 8px rgba(black, 0.1));
}

Content Blocks (@content)

The @content directive lets a mixin accept an entire block of styles passed by the caller, most commonly used for media-query wrappers.

responsive.scss

@mixin respond($breakpoint) {
  @media (min-width: $breakpoint) {
    @content;
  }
}

.container {
  width: 100%;

  @include respond(768px) {
    width: 750px;
  }
}

Passing Arguments to @content

Dart Sass allows @content to pass arguments into the block supplied by the caller, using matching parentheses on both sides.

content-args.scss

@mixin each-breakpoint($breakpoints) {
  @each $name, $width in $breakpoints {
    @media (min-width: $width) {
      @content($name);
    }
  }
}

@include each-breakpoint(('sm': 576px, 'md': 768px)) using ($name) {
  .visible-#{$name} { display: block; }
}

Functions

Custom functions, defined with @function, compute and @return a value rather than emitting CSS declarations directly.

functions.scss

@use 'sass:math';

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

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

Recursive Functions

recursive.scss

@function sum-list($list) {
  $total: 0;
  @each $item in $list {
    $total: $total + $item;
  }
  @return $total;
}

$widths: 10px, 20px, 30px;
$total-width: sum-list($widths); // 60px

Extend & Placeholder Selectors

@extend lets one selector inherit the styles of another at the selector level (not by duplicating declarations). Combined with placeholder selectors (prefixed %), it avoids emitting unused, standalone CSS rules.

extend.scss

%btn-base {
  padding: 0.5rem 1rem;
  border-radius: 4px;
  border: none;
}

.btn-primary {
  @extend %btn-base;
  background: $primary-color;
}

.btn-secondary {
  @extend %btn-base;
  background: gray;
}

@extend with !optional

By default, Sass throws an error if an @extend target doesn't exist anywhere in the stylesheet. Appending !optional suppresses that error.

optional-extend.scss

.message {
  @extend .alert !optional;
}

Caution

Overusing @extend across unrelated selectors can create unexpected specificity and unwieldy selector lists in the compiled output — prefer mixins for most reuse, reserving @extend for truly shared base styles.

Control Directives

@if / @else if / @else

conditionals.scss

@use 'sass:color';

@mixin text-color($bg) {
  @if color.lightness($bg) > 70% {
    color: black;
  } @else if color.lightness($bg) > 40% {
    color: darkgray;
  } @else {
    color: white;
  }
}

@each

@each can iterate over simple lists, or destructure lists-of-lists and maps into multiple loop variables.

each.scss

$icons: 'home', 'search', 'settings';

@each $icon in $icons {
  .icon-#{$icon} {
    background-image: url('icons/#{$icon}.svg');
  }
}

// Destructuring a map
$theme-colors: ('primary': blue, 'danger': red);
@each $name, $color in $theme-colors {
  .text-#{$name} { color: $color; }
}

@for

@for supports both through (inclusive of the end value) and to (exclusive of it).

for.scss

@use 'sass:math';

@for $i from 1 through 5 {
  .col-#{$i} {
    width: math.div(100%, 5) * $i;
  }
}

@while

while.scss

$i: 1;
@while $i <= 3 {
  .mt-#{$i} { margin-top: $i * 4px; }
  $i: $i + 1;
}

Warning

Always ensure the loop condition in @while eventually becomes false — an incorrect condition produces an infinite loop at compile time.

Debugging & Error Handling

Sass provides three directives for surfacing information and errors during compilation.

debugging.scss

@mixin spacing($size) {
  @if not (unitless($size) == false) {
    @error "Expected a value with units, got #{$size}.";
  }
  @warn "spacing mixin is deprecated, use gap() instead.";
  @debug "computed size: #{$size}";
  margin: $size;
}
DirectivePurposeHalts Compilation?
@debugPrint a value to the console for inspectionNo
@warnPrint a warning, typically for deprecationsNo
@errorAbort compilation with a messageYes

Operators

Sass supports arithmetic, comparison, and boolean operators directly in property values and expressions.

operators.scss

@use 'sass:math';

.box {
  width: 100px + 50px;      // addition
  height: math.div(200px, 4); // division
  margin: -$spacing-unit;     // negation
  z-index: 2 * 5;             // multiplication

  @if (10px > 5px) and (true == true) {
    border: 1px solid black;
  }
}

Important

Direct use of the / operator for division is deprecated in Dart Sass — always use math.div() instead.

Data Types

  • Numbers — 1.5, 10px, 3rem, with or without units.
  • Strings — quoted ("bold") or unquoted (bold).
  • Colors — #fff, rgba(), hsl(), named colors like tomato.
  • Booleans — true / false.
  • Lists — space- or comma-separated values, optionally bracketed.
  • Maps — key-value pairs, e.g. (key: value).
  • null — represents the absence of a value.

Strings & Interpolation

Interpolation with # injects a Sass expression into a selector, property name, string, or value.

interpolation.scss

$side: 'top';
.mt-#{$side} { margin-#{$side}: 1rem; }

@use 'sass:string';
$upper: string.to-upper-case('hello'); // 'HELLO'

Lists

lists.scss

@use 'sass:list';

$fonts: Helvetica, Arial, sans-serif;
$sizes: 4px 8px 16px;

.text { font-family: list.nth($fonts, 1); }

$appended: list.append($sizes, 32px); // 4px 8px 16px 32px
$joined: list.join(1px 2px, 3px 4px); // 1px 2px 3px 4px
$length: list.length($fonts); // 3

Maps

maps.scss

@use 'sass:map';

$theme-colors: (
  'primary': #3498db,
  'success': #2ecc71,
  'danger': #e74c3c
);

.alert {
  background: map.get($theme-colors, 'danger');
}

$merged: map.merge($theme-colors, ('warning': orange));
$keys: map.keys($theme-colors); // 'primary', 'success', 'danger'
$has: map.has-key($theme-colors, 'primary'); // true

Built-in Modules Reference

Dart Sass ships built-in modules that must be loaded explicitly with @use 'sass:module-name' before their functions can be used.

ModulePurposeExample Functions
sass:mathArithmetic and numeric utilitiesmath.div(), math.round(), math.min(), math.clamp()
sass:colorColor manipulationcolor.adjust(), color.scale(), color.mix()
sass:listList operationslist.append(), list.length(), list.index()
sass:mapMap operationsmap.get(), map.merge(), map.remove()
sass:stringString manipulationstring.to-upper-case(), string.slice()
sass:selectorSelector introspectionselector.is-superselector()
sass:metaIntrospect Sass itselfmeta.type-of(), meta.call()

sass:math in Depth

math-module.scss

@use 'sass:math';

math.div(10, 2);      // 5
math.round(4.6);      // 5
math.ceil(4.1);       // 5
math.floor(4.9);      // 4
math.abs(-8px);        // 8px
math.min(1px, 4px, 2px); // 1px
math.max(1px, 4px, 2px); // 4px
math.clamp(0, 15, 10); // 10 (clamps value between min and max)
math.percentage(math.div(1, 4)); // 25%

sass:color in Depth

color-module.scss

@use 'sass:color';

color.adjust($primary-color, $lightness: -10%);
color.scale($primary-color, $lightness: 20%);
color.mix(blue, red, 50%);
color.lightness($primary-color);
color.complement($primary-color);

sass:meta in Depth

The meta module allows introspecting types, checking whether mixins/functions exist, and even calling functions dynamically.

meta-module.scss

@use 'sass:meta';

meta.type-of(10px);        // 'number'
meta.type-of('hi');        // 'string'
meta.function-exists('rem'); // true / false
meta.mixin-exists('flex-center');

$fn: meta.get-function('rem');
meta.call($fn, 24px);

@at-root

@at-root lets a nested rule escape its ancestor selectors and be placed at the document root of the generated CSS — useful for breaking out of deep nesting contexts.

at-root.scss

.parent {
  color: red;

  @at-root .unnested {
    color: blue;
  }

  @at-root {
    .sibling-one { color: green; }
    .sibling-two { color: purple; }
  }
}

Media Query Merging

Dart Sass automatically merges nested @media rules using logical and, rather than emitting duplicate, nested media blocks.

media-merge.scss

@media (min-width: 768px) {
  .sidebar {
    @media (orientation: landscape) {
      width: 300px;
    }
  }
}
// Compiles to:
// @media (min-width: 768px) and (orientation: landscape) { .sidebar { width: 300px; } }

Architecture: The 7-1 Pattern

A popular, scalable way to organize large Sass codebases is the 7-1 pattern: seven folders, one main entry file.

scss
abstracts
_variables.scss
_functions.scss
_mixins.scss
main.scss

main.scss

@forward 'abstracts/variables';
@forward 'abstracts/mixins';

@use 'base/reset';
@use 'base/typography';
@use 'components/buttons';
@use 'components/cards';
@use 'layout/header';
@use 'layout/footer';
@use 'pages/home';

Testing Sass Code

Libraries like sass-true let you write unit tests for mixins and functions, asserting compiled CSS output or return values.

_rem.test.scss

@use 'sass:test';
@use 'true' as *;
@use '../abstracts/functions' as *;

@include describe('rem()') {
  @include it('converts px to rem') {
    @include assert-equal(rem(32px), 2rem);
  }
}

Common Pitfalls

  1. Using the bare / operator for division instead of math.div().
  2. Mixing @import and @use in the same codebase, causing confusion over load order.
  3. Over-nesting selectors, producing unnecessarily specific and hard-to-override CSS.
  4. Extending across unrelated selectors, bloating the compiled selector lists.
  5. Forgetting !default on configurable library variables, breaking downstream customization.

Best Practices

  1. Prefer @use / @forward over the deprecated @import.
  2. Keep nesting shallow — 3 levels max as a general rule of thumb.
  3. Use %placeholders for shared, non-parametric styles rather than classes solely meant for extension.
  4. Organize large projects with the 7-1 pattern or a similar convention.
  5. Name variables and mixins descriptively by purpose, not by their literal value.
  6. Configure shareable modules with with (...) instead of forking library code.

Best Practice

Run Sass output through a linter like Stylelint with the stylelint-scss plugin to enforce these conventions automatically in CI.

History & Evolution

Conclusion

Sass extends CSS with everything needed to build scalable, maintainable stylesheets: variables, nesting, mixins, functions, extends, control flow, and a rich built-in module system. Mastering @use/@forward, module configuration, the 7-1 architecture, and the built-in math, color, list, and map modules will let your CSS stay clean and predictable even as a codebase grows substantially.

Summary

Start simple with variables and nesting, then progressively layer in mixins, functions, modules, and architecture patterns as your stylesheets grow in scale and complexity.

For the authoritative, always up-to-date reference, see the official Sass documentation.