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
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| Feature | SCSS | Sass (Indented) |
|---|---|---|
| File extension | .scss | .sass |
| Braces & semicolons | Required | Not used |
| Whitespace significance | No | Yes |
| Comments | // and /* */ | // and /* |
| Adoption | Most common | Rare |
Tip
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 Type | Expanded Output | Compressed Output |
|---|---|---|
| // silent | Removed | Removed |
| /* */ loud | Kept | Removed, unless it starts with /*! |
Tip
Best Practice
Sass vs LESS vs Stylus
| Feature | Sass | LESS | Stylus |
|---|---|---|---|
| Runtime | Dart / standalone binary | JavaScript | JavaScript |
| Module system | @use/@forward | Basic @import | Basic @import |
| Control flow | @if, @each, @for, @while | Guards / mixins | Conditionals |
| Maturity | Most mature ecosystem | Mature | Smaller 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 sassterminal
yarn add -D sassterminal
pnpm add -D sassterminal
sass --versionCompiling 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.cssTip
How Compilation Works
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 sassmain.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
- Open VS Code and go to the Extensions panel (Ctrl+Shift+X on Windows/Linux, Cmd+Shift+X on macOS).
- Search for Live Sass Compiler by Glenn Marks.
- Click Install.
- 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.
Tip
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
| Format | Description |
|---|---|
| expanded | Fully readable, multi-line CSS â good for development. |
| compressed | Minified, single-line CSS â ideal for production. |
CLI vs Extension
| Aspect | Dart Sass CLI | Live Sass Compiler Extension |
|---|---|---|
| Setup | Requires npm/global install | Point-and-click install from marketplace |
| Best for | Production builds, CI pipelines, build tool integration | Quick projects, learning, static sites without a build step |
| Configuration | CLI flags / build tool config | settings.json |
| Module system support | Full @use/@forward support | Full support (uses Dart Sass under the hood) |
Note
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 globallyWarning
Variable Scope Rules
- Variables declared at the top level of a stylesheet are global.
- Variables declared inside a rule, mixin, or function are local to that block.
- A local variable with the same name as a global one will shadow it within that block only.
- 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
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
@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; // publicImportant
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-migratorRunning 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.scssTip
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.scssPost-Migration Checklist
- Re-run your full build and visually diff the compiled CSS output against the pre-migration version.
- Search for any remaining @import statements the tool flagged as unsafe to auto-convert.
- Check for duplicate namespace conflicts, especially in deeply nested partial structures.
- Remove now-unused global variable duplication that @import previously allowed.
Warning
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); // 60pxExtend & 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
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
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;
}| Directive | Purpose | Halts Compilation? |
|---|---|---|
| @debug | Print a value to the console for inspection | No |
| @warn | Print a warning, typically for deprecations | No |
| @error | Abort compilation with a message | Yes |
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
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); // 3Maps
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'); // trueBuilt-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.
| Module | Purpose | Example Functions |
|---|---|---|
| sass:math | Arithmetic and numeric utilities | math.div(), math.round(), math.min(), math.clamp() |
| sass:color | Color manipulation | color.adjust(), color.scale(), color.mix() |
| sass:list | List operations | list.append(), list.length(), list.index() |
| sass:map | Map operations | map.get(), map.merge(), map.remove() |
| sass:string | String manipulation | string.to-upper-case(), string.slice() |
| sass:selector | Selector introspection | selector.is-superselector() |
| sass:meta | Introspect Sass itself | meta.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.
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
- Using the bare / operator for division instead of math.div().
- Mixing @import and @use in the same codebase, causing confusion over load order.
- Over-nesting selectors, producing unnecessarily specific and hard-to-override CSS.
- Extending across unrelated selectors, bloating the compiled selector lists.
- Forgetting !default on configurable library variables, breaking downstream customization.
Best Practices
- Prefer @use / @forward over the deprecated @import.
- Keep nesting shallow â 3 levels max as a general rule of thumb.
- Use %placeholders for shared, non-parametric styles rather than classes solely meant for extension.
- Organize large projects with the 7-1 pattern or a similar convention.
- Name variables and mixins descriptively by purpose, not by their literal value.
- Configure shareable modules with with (...) instead of forking library code.
Best Practice
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
For the authoritative, always up-to-date reference, see the official Sass documentation.