What is Sass?
Sass stands for Syntactically Awesome Style Sheets. It is a CSS preprocessor — a language that extends plain CSS with programming features and compiles down to standard CSS that browsers can understand. If you have ever found yourself repeating colour values, duplicating media queries or wading through hundreds of lines of flat CSS, Sass is the tool that solves those problems.
Created by Hampton Catlin in 2006, Sass was the first CSS preprocessor to gain widespread adoption. It introduced concepts from general-purpose programming languages — variables, nesting, functions, loops and modules — into the world of stylesheets. Today it is the foundation of projects like Bootstrap, Foundation and countless design systems.
The best way to think about Sass is as CSS with a toolbox. You write .scss or .sass files using Sass features, run them through a compiler, and get back plain .css files the browser can load. There is no runtime cost. The power exists entirely in your development workflow.
Sass vs CSS: what changes?
Plain CSS is a declarative language — you describe how elements should look, but you cannot create abstractions, reuse logic or organise code beyond putting things in separate files. Sass addresses every one of those limitations:
| Capability | Plain CSS | Sass |
|---|---|---|
| Variables | Custom properties (--var) |
$variable with full scoping |
| Nesting | Basic (now in CSS) | Mature, with parent selectors (&) |
| Reuse | Copy-paste or utility classes | Mixins, functions and extends |
| Organisation | @import only |
@use, @forward, partials |
| Logic | None | @if, @each, @for, @while |
| Maths | calc() only |
Full arithmetic on any values |
| Colours | Fixed values | lighten, darken, mix and more |
Sass vs SCSS syntax
Sass supports two syntaxes:
SCSS (.scss) — the most common. It uses curly braces and semicolons, just like CSS. Any valid CSS file is automatically valid SCSS. This makes adoption seamless:
/* style.scss */
$primary: #3498db;
.button {
background: $primary;
color: white;
&:hover {
background: darken($primary, 10%);
}
}
Indented syntax (.sass) — the original. It uses indentation and newlines instead of braces and semicolons, making it more concise but less familiar:
/* style.sass */
$primary: #3498db
.button
background: $primary
color: white
&:hover
background: darken($primary, 10%)
SCSS is the syntax you will encounter in virtually every modern project. Unless you have a specific reason to choose indented Sass, use .scss.
Why Sass exists
Solving real problems is why Sass became the industry standard. The core issues it addresses:
Repetition. Without variables, a single colour change means Find-and-Replace across every file. Sass variables let you change a value once and update it everywhere.
Flat structure. Deeply nested components produce long, repetitive selectors like .card .card-header .card-title. Sass nesting mirrors the HTML hierarchy in your styles.
No reuse. CSS has no concept of a reusable block of styles. A button component with 10 variants means 10 nearly identical blocks. Sass mixins and extends eliminate the duplication.
No logic. Generating a colour palette, alternating row colours or creating responsive breakpoints from a list requires external tools. Sass gives you loops and conditionals right in the stylesheet.
Organisation. Importing dozens of CSS files with @import creates a flat global namespace where names inevitably collide. Sass modules (@use, @forward) solve this with namespacing.
Variables
Variables are the simplest and most impactful Sass feature. A variable stores a value you can reuse throughout your stylesheet:
/* style.scss */
$primary: #3498db;
$secondary: #2ecc71;
$font-stack: 'Helvetica Neue', Arial, sans-serif;
$spacing-unit: 8px;
body {
font-family: $font-stack;
}
.container {
padding: $spacing-unit * 3;
}
.btn-primary {
background: $primary;
color: white;
}
Change $primary once at the top and every element that references it updates automatically. Sass variables are compile-time constants — they do not exist in the output CSS. This is different from CSS custom properties (--var), which live in the browser at runtime. Use Sass variables for values that do not need to change dynamically.
Variables have scope. A variable declared inside a rule or mixin is local to that block. Use !global to override a global variable from within a local scope, though this is rarely necessary:
/* style.scss */
$color: red; // global
.box {
$color: blue; // local — only exists inside .box
color: $color;
}
// $color is still red here
Nesting
Nesting lets you write child selectors inside their parent, mirroring the HTML structure and keeping related styles grouped together:
/* style.scss */
.nav {
display: flex;
gap: $spacing-unit * 2;
&__item {
padding: $spacing-unit;
&--active {
font-weight: bold;
border-bottom: 2px solid $primary;
}
&:hover {
background: rgba($primary, 0.1);
}
}
&__logo {
margin-right: auto;
}
}
This compiles to:
/* compiled.css */
.nav { display: flex; gap: 16px; }
.nav__item { padding: 8px; }
.nav__item--active { font-weight: bold; border-bottom: 2px solid #3498db; }
.nav__item:hover { background: rgba(52, 152, 219, 0.1); }
.nav__logo { margin-right: auto; }
The & parent selector is essential. It references the parent selector, so &__item inside .nav becomes .nav__item. Without &, Sass treats the ampersand as the parent context. You can also use & for pseudo-classes and pseudo-elements instead of nesting them as separate blocks.
While native CSS now supports basic nesting, Sass nesting is more mature — it supports & for arbitrary parent references, works everywhere and has been battle-tested for over a decade.
Partials and imports
Partials are Sass files that are meant to be imported, not compiled on their own. Name them with an underscore prefix — _variables.scss, _buttons.scss, _mixins.scss — and Sass will skip them during direct compilation:
// _variables.scss
$primary: #3498db;
$spacing: 8px;
$font-stack: 'Helvetica Neue', Arial, sans-serif;
// _buttons.scss
@use 'variables' as *;
.button {
padding: $spacing * 2;
background: $primary;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
// main.scss
@use 'variables' as *;
@use 'buttons';
@use 'layout';
@use 'typography';
The @use rule imports a file as a module. You then access its members through a namespace, or use as * to bring everything into scope. This keeps your global namespace clean and prevents naming collisions between files.
Modules: @use and @forward
The @import rule is deprecated. It merged all files into one global scope, meaning a $primary in one file could silently overwrite a $primary in another. The modern replacements are @use and @forward.
@use
@use imports a module and gives it a namespace:
/* style.scss */
@use 'variables';
.button {
background: variables.$primary;
padding: variables.$spacing * 2;
}
You can alias the namespace or bring all members into scope:
/* style.scss */
@use 'variables' as v;
@use 'variables' as *; // no namespace needed
@forward
@forward re-exports a module’s members so that files importing the current module get access to them. It is used to create barrel files — a single entry point that exposes multiple modules:
// _index.scss
@forward 'variables';
@forward 'mixins';
@forward 'functions';
// main.scss
@use 'index' as *; // gets everything from variables, mixins and functions
Use @forward with show and hide to control which members are re-exported:
/* style.scss */
@forward 'variables' show $primary, $secondary;
@forward 'mixins' hide $internal-helper;
Mixins
Mixins are reusable blocks of styles you can parameterise and include anywhere. Think of them as functions that output CSS declarations:
/* mixins.scss */
@mixin flex-center {
display: flex;
justify-content: center;
align-items: center;
}
@mixin respond-to($breakpoint) {
@if $breakpoint == 'tablet' {
@media (min-width: 768px) { @content; }
} @else if $breakpoint == 'desktop' {
@media (min-width: 1024px) { @content; }
}
}
.hero {
@include flex-center;
min-height: 100vh;
@include respond-to('desktop') {
min-height: 80vh;
}
}
.card {
@include flex-center;
padding: 1rem;
}
Mixins can have default parameters, accept any number of arguments and use @content to accept a block of styles from the caller. They are the primary abstraction mechanism in Sass — design systems, responsive helpers and cross-browser compatibility layers are all built on mixins.
Functions
Sass functions compute and return a value. They are similar to mixins but instead of outputting CSS they produce a value you can assign:
/* functions.scss */
@function spacing($n) {
@return $n * 8px;
}
@function shade($color, $percentage) {
@return mix(black, $color, $percentage);
}
.hero {
padding: spacing(4); // 32px
background: shade(#3498db, 20%);
}
.sidebar {
padding: spacing(2); // 16px
}
Sass ships with many built-in functions for colours (lighten, darken, saturate, adjust-hue), strings (str-length, str-insert), maths (abs, min, max, round) and lists (length, nth, append, index). You can write your own for project-specific logic.
Extends and inheritance
The @extend rule lets one selector inherit the styles of another. Sass merges the two selectors into a single rule in the output CSS:
/* style.scss */
%message {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.success {
@extend %message;
background: #d4edda;
color: #155724;
}
.error {
@extend %message;
background: #f8d7da;
color: #721c24;
}
.warning {
@extend %message;
background: #fff3cd;
color: #856404;
}
Compiles to:
/* compiled.css */
.success, .error, .warning {
padding: 1rem;
border-radius: 4px;
margin-bottom: 1rem;
}
.success { background: #d4edda; color: #155724; }
.error { background: #f8d7da; color: #721c24; }
.warning { background: #fff3cd; color: #856404; }
Placeholders (selectors starting with %) are not output to CSS on their own — they only appear when extended. This avoids empty rules in your compiled output.
Use @extend for shared structural patterns. For anything requiring parameters, use a mixin instead — @extend cannot accept arguments.
Operators
Sass supports the standard arithmetic operators for working with numbers:
/* style.scss */
$column-count: 12;
$gutter: 24px;
$gap: $gutter / 2; // 12px
$base: 16px;
$heading-1: $base * 2; // 32px
$heading-2: $base * 1.5; // 24px
$full-width: 100%;
$sidebar: $full-width / 3; // 33.333%
// Remainder
$remainder: 17 % 5; // 2
Sass also supports string interpolation, comparison operators and boolean operators:
/* style.scss */
$primary: #3498db;
$dark: darken($primary, 20%);
$is-dark: true;
@mixin theme-bg {
@if $is-dark {
background: $dark;
} @else {
background: $primary;
}
}
Be aware that division (/) is now treated as a CSS fraction in some contexts (e.g. font: 16px/1.5). Wrap divisions in math.div() to avoid ambiguity, or use calc() for dynamic division.
Control flow
Sass includes conditionals and loops that generate CSS at compile time:
@if / @else
/* style.scss */
$theme: 'light';
@mixin theme-bg {
@if $theme == 'light' {
background: white;
color: black;
} @else if $theme == 'dark' {
background: #1a1a2e;
color: white;
} @else {
background: #f0f0f0;
color: #333;
}
}
@each
Iterates over a list or map:
/* style.scss */
$colors: (
primary: #3498db,
secondary: #2ecc71,
danger: #e74c3c,
);
@each $name, $color in $colors {
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background: $color;
}
}
@for
Generates a range of selectors:
/* style.scss */
@for $i from 1 through 12 {
.col-#{$i} {
width: ($i / 12) * 100%;
}
}
@for $i from 1 through 5 {
.mt-#{$i} {
margin-top: $i * 8px;
}
}
@while
Loops while a condition is true:
/* style.scss */
$columns: 12;
$i: $columns;
@while $i > 0 {
.col-#{$i} {
width: ($i / $columns) * 100%;
}
$i: $i - 1;
}
Maps and lists
Sass has first-class data structures for collections.
Lists are ordered, comma or space separated:
/* style.scss */
$breakpoints: 480px, 768px, 1024px, 1280px;
@each $bp in $breakpoints {
@media (min-width: $bp) {
.container {
max-width: $bp;
}
}
}
// Lists can be nested
$margins: (8px, 16px, 24px, 32px);
Maps are key-value pairs, similar to objects or dictionaries:
/* style.scss */
$theme: (
primary: #3498db,
secondary: #2ecc71,
bg: white,
text: #333,
);
@each $key, $value in $theme {
--color-#{$key}: #{$value};
}
You can access map values with map-get, merge with map-merge, check existence with map-has-key and iterate with @each.
Sass built-in functions
Sass ships with a rich standard library:
/* style.scss */
// Colour manipulation
$primary: #3498db;
.lighter { color: lighten($primary, 20%); }
.darker { color: darken($primary, 15%); }
.desaturated { color: desaturate($primary, 30%); }
.hue-shifted { color: adjust-hue($primary, 30deg); }
// Colour functions
$alpha: rgba($primary, 0.5); // semi-transparent
$contrast: contrast-color($primary); // contrast-safe colour
// Maths
$rounded: round(3.14); // 3
$limited: clamp(1, 10, 5); // 5
$half: math.div(100%, 2); // 50%
// Strings
$name: to-upper-case('sass'); // SASS
$length: str-length('hello'); // 5
$joined: str-insert('hello', ' world', 6); // 'hello world'
// Lists
$items: append(1px, 2px, 3px, 4px);
$first: nth($items, 1); // 1px
$count: length($items); // 4
These functions are available globally in SCSS and through @use 'sass:color', @use 'sass:math', @use 'sass:string', @use 'sass:list' and @use 'sass:map'.
How to use Sass
Dart Sass
Dart Sass is the canonical and only actively maintained Sass implementation. Install it with:
npm install sass
Then compile files directly:
npx sass src/styles.scss dist/styles.css
Build tool integration
In most modern projects Sass is handled by your bundler. Vite, webpack, Parcel and esbuild all support Sass out of the box:
Vite — just install sass and import .scss files:
import './styles.scss';
webpack — add sass-loader:
module.exports = {
module: { rules: [{ test: /\.scss$/, use: ['style-loader', 'css-loader', 'sass-loader'] }] }
};
CLI compilation
# Watch mode — recompile on every save
npx sass --watch src:dist
# One compile
npx sass src/styles.scss dist/styles.css
# Output compressed CSS
npx sass --style=compressed src/styles.scss dist/styles.css
Best practices
- Use SCSS syntax. It is CSS-compatible, widely adopted and far more readable than the indented syntax.
- Use
@useand@forward, never@import. The import system is deprecated and causes global scope pollution. - Organise with partials. One concern per file:
_variables.scss,_mixins.scss,_buttons.scss,_layout.scss. - Namespace your modules. Use
@use 'module' as varor let the default namespace (module.$var) clarify where values come from. - Use placeholders for shared patterns.
%placeholderselectors only emit CSS when extended, keeping output lean. - Write parameterised mixins over extends. Mixins are more flexible and produce explicit, predictable CSS.
- Keep business logic out of stylesheets. Sass functions and loops belong in your build pipeline, not in your component logic.
- Lint your Sass. Use
stylelintwith a Sass plugin to catch deprecations and enforce conventions. - Output compressed CSS for production. Use
--style=compressedor let your bundler minify. - Avoid deeply nested selectors. Two to three levels is enough. Deeper nesting usually signals a naming or structure problem.