Sass/SCSS Cheatsheet - Sass Syntax & Mixin Reference
Once a stylesheet fills with repeated declarations, Sass earns its place by extracting values and fragments rather than by nicer syntax. Scales go into variables and Maps, colour maths into functions, repeated declaration sequences into mixins, and reusable selector blocks into % placeholders consumed by @extend. Pick SCSS over the indented syntax because SCSS is a superset of CSS and existing files convert cleanly, and load modules with @use rather than @import, since repeated imports measurably slow compilation. Sections cover variables and arithmetic, Maps and colour functions, nesting, mixins and inheritance, functions and loops, conditionals, and module imports. The judgement rule is simple: if a change should land in exactly one place, extract a variable or mixin; if any selector may reuse the block, reach for @extend; if the fragment needs parameters or logic, it has to be a mixin. Check how much duplicated selector output each choice leaves behind before combining them.
Typical Use Case
For front-end engineers maintaining a mid-sized design system or component library: one palette, one spacing scale, breakpoints in one place, and no selector nesting past the point of review. Covers variables with !default, maps and colour functions, nesting with @at-root, mixin versus @extend, functions and loops, and modular loading with @use and @forward against the deprecated @import. Two judgements: @extend multiplies selector combinations, so prefer mixins, and use @use only.
Variables & Operations 5
$primary: #6366f1color: $primary$primary: #6366f1 !defaultwidth: 100px + 50pxcontent: "icon-#{$name}"Maps & Color Functions 8
$sizes: (small: 12px, large: 24px)width: map-get($sizes, small)map-keys($sizes)map-merge($sizes, (medium: 16px))color: darken(#6366f1, 10%)color: lighten(#6366f1, 10%)color: mix(red, blue)color: rgba(#6366f1, 0.5)Nesting 4
nav { ul { margin: 0 } }a { &:hover { color: red } }.btn { &-primary { color: blue } }.box { @at-root .inner { } }Mixins & Inheritance 8
@mixin flex-center { display: flex; justify-content: center }@include flex-center@mixin responsive($breakpoint) { @media ... }@mixin button { @content }.btn { @include button { color: red } }%message-box { border: 1px solid }@extend %message-box.error { @extend %message-box }Functions & Loops 4
@function px-to-rem($px) { @return $px / 16 * 1rem }@for $i from 1 through 3 { .col-#{$i} { } }@each $color in red, blue, green { .#{$color} { } }@while $i > 0 { .item-#{$i} { } $i: $i - 1 }Conditionals & Control 5
@if $type == dark { background: black }@else if $type == light { background: white }@else { background: gray }@warn "Variable not defined"@error "Invalid argument"Imports & Modules 6
@import "variables"@use "variables" as *@use "variables" as vars@use "config" with ($primary: red)@forward "variables"@import url("https://fonts.googleapis.com")Parameter matrix
| Parameter | Effect | Example |
|---|---|---|
$primary: #6366f1 | Declares a Sass variable. The assignment exists only at compile time: it never reaches the browser and adds no runtime CSS of its own. | $primary: #6366f1 |
color: $primary | Reads a variable inside a property value. This substitution is the whole basis of a single-source colour or spacing palette. | color: $primary |
$primary: #6366f1 !default | Only wins when the variable has not been assigned yet, which makes it the conventional way for a library to ship an overridable default. | $primary: #6366f1 !default |
width: 100px + 50px | Same-unit values add directly. Mixing incompatible units is a compile error, so convert first or hand the calculation to calc. | width: 100px + 50px |
#{$name} | Interpolation splices a variable into a selector, a property name or a string. Nothing that loops over a list can build class names without it. | content: "icon-#{$name}" |
$sizes: (small: 12px, large: 24px) | Stores key/value pairs in a Map instead of a pile of loose variables, so lookups stay meaningful instead of positional. | $sizes: (small: 12px, large: 24px) |
map-get($sizes, small) | Fetches a value by key. A missing key yields null rather than a build failure, which is convenient but easy to let leak into CSS. | width: map-get($sizes, small) |
map-keys($sizes) | Returns every key, typically to drive @each over the whole Map rather than over a hardcoded list. | map-keys($sizes) |
map-merge($sizes, (medium: 16px)) | Merges two Maps into a new one, letting a local scope override individual scale values without mutating the original palette. | map-merge($sizes, (medium: 16px)) |
darken / lighten | Shifts lightness by a fixed ratio. Because the math is linear rather than perceptually uniform, the result often looks weaker than the number suggests. | color: darken(#6366f1, 10%) |
mix(red, blue) | Blends two colours by weight and returns the intermediate tone, the quickest way to derive a hover state from a base colour. | color: mix(red, blue) |
rgba(#6366f1, 0.5) | Builds an rgba colour from a colour value plus an alpha, removing the class of mistakes that come from hand-maintaining two separate values. | color: rgba(#6366f1, 0.5) |
nav { ul { margin: 0 } } | Nesting compiles down to descendant selectors, which is what makes scoped authoring pleasant. Nest too deep and the emitted selectors bloat. | nav { ul { margin: 0 } } |
&:hover | The ampersand stands for the parent selector, which is how pseudo-classes and pseudo-elements are attached without repeating the parent. | a { &:hover { color: red } } |
&-primary | Concatenating straight onto the ampersand mints a sibling class name such as .btn-primary, so the shared prefix is written once. | .btn { &-primary { color: blue } } |
@at-root | Breaks out of the current nesting to emit a selector at the top level. Required whenever a third-party selector or a strict BEM block has to land at the root. | .box { @at-root .inner { } } |
@mixin flex-center { ... } | Defines a reusable fragment of declarations. It produces no selector by itself and only expands where it is included. | @mixin flex-center { display: flex; justify-content: center } |
@include flex-center | Expands a mixin at the call site. Arguments, when the mixin takes any, go inside the parentheses. | @include flex-center |
@mixin responsive($breakpoint) { @media ... } | Wraps @media inside a mixin so breakpoints live in one place instead of being hardcoded at every call site. | @mixin responsive($breakpoint) { @media ... } |
@mixin button { @content } | Reserves a content block with @content, turning a mixin into a template the caller fills in. | @mixin button { @content } |
@include button { color: red } | Passes a block of declarations into a mixin that declares @content. | .btn { @include button { color: red } } |
%message-box | A placeholder selector: it defines styles but emits nothing, existing purely as a target for @extend. | %message-box { border: 1px solid } |
@extend %message-box | Folds the placeholder rules into the current selector. Because it rewrites selector combinations, prefer a mixin whenever the blast radius is hard to reason about. | .error { @extend %message-box } |
@function px-to-rem($px) { @return ... } | Defines a callable Sass function used inside style values. Like variables, it vanishes once compilation finishes. | @function px-to-rem($px) { @return $px / 16 * 1rem } |
@for $i from 1 through 3 | Loops over a numeric range; paired with interpolation this is what emits .col-1, .col-2 and .col-3. | @for $i from 1 through 3 { .col-#{$i} { } } |
@each $color in red, blue, green | Walks a list or the keys of a Map, generating a family of rules that differ only by a value. | @each $color in red, blue, green { .#{$color} { } } |
@while $i > 0 | Condition-driven looping. It is more flexible than @for and equally easy to turn into an infinite loop, so production code usually reaches for @for first. | @while $i > 0 { .item-#{$i} { } $i: $i - 1 } |
@if / @else if / @else | Compile-time branching, the mechanism for switching a theme or a size scale from a configuration variable. | @if $type == dark { background: black } |
@warn | Prints a warning and keeps compiling. Appropriate for non-fatal problems such as a variable that was never assigned. | @warn "undefined variable" |
@error | Aborts compilation with a message, for the cases where bad input genuinely has to fail the build. | @error "invalid argument" |
@use "variables" | Loads a module. Namespaces are isolated, so members are reached through a prefix; this is the form the Sass team recommends. | @use "variables" |
@use "variables" as * | Merges the module members into the current scope, which approximates the ergonomics of the old @import but only for this one file. | @use "variables" as * |
@use "variables" as vars | Gives the module a namespace alias, so members are accessed as vars.$primary. | @use "variables" as vars |
@use "config" with ($primary: red) | Any !default variable can be overridden at load time, which is the sanctioned way to inject configuration into a module. | @use "config" with ($primary: red) |
@forward "variables" | Re-exports this file members upward so a single entry point can aggregate many modules. | @forward "variables" |
@import "variables" | The legacy import: global scope, and repeated imports emit the same CSS again. Officially deprecated. | @import "variables" |
@import url("https://fonts.googleapis.com") | Left untouched as plain CSS output, which is how third-party stylesheet that Sass cannot express get in. | @import url("https://fonts.googleapis.com") |
Common pitfalls
SymptomThe build prints a deprecation warning about @import and says it will be removed in a future release.
CauseLegacy projects still lean on @import everywhere; Dart Sass folds repeated @import statements into @use and warns while it does so.
FixWrite new code with @use and @forward. Migrate an existing codebase one module at a time, file by file, instead of in one sweeping rewrite.
SymptomThe same styles end up bundled twice and the CSS payload doubles.
CauseSeveral files @import the same module, and @import neither isolates namespaces nor de-duplicates what it loads.
FixSwitch to @use so the module is loaded once, and let several entry points re-export through @forward.
SymptomAfter an @extend, unrelated rules elsewhere in the stylesheet also get rewritten.
Cause@extend merges the selector into every match; with deep nesting and many placeholder selectors the number of combinations grows fast.
FixExpress shared styles as a parameterised mixin called with @include, and keep @extend only for selector structures whose impact you can state precisely.
Symptomdarken returns a colour that looks barely different, or differs without reaching the contrast you wanted.
Causedarken and lighten adjust lightness linearly, which does not track human perception; the real requirement is usually a specific contrast against white.
FixSwitch to color.adjust in modern Sass with an explicit lightness target, or reach for a colour ramp that the design system has already validated.
SymptomA variable was clearly assigned somewhere, yet other files read it as empty.
CauseModules loaded with @use live in their own namespace, so members must be written names.$var; a bare $var is simply undefined there.
FixAlways go through the namespace, or adopt as * in a file that genuinely needs the convenience and accept that it is file-local.
SymptomOverriding a default has no effect and the library value still wins.
Cause!default applies only while the variable is still unassigned; assigning it earlier in the same file means the later @use can no longer override it.
FixPass the value at load time with @use "config" with ($primary: red), which is the configuration route the Sass team documents.
SymptomCompilation crawls and the emitted CSS repeats a large number of selectors.
CauseDeep nesting stacked on @extend explodes the combination count; another common source is a selector written inside a nesting block where a declaration list was meant.
FixCap nesting depth (three levels is a reasonable limit), express shared styles as mixins, and use source maps to trace where the duplicates originate.
SymptomThe compiler reports Undefined variable even though the variable is defined in one of the files.
CauseA load-order problem: the variable is only defined after its first use, and Sass evaluates strictly in source order.
FixCollect variables in one entry file and load it first, using @forward to make the dependency order explicit.
Troubleshooting
1Locate every deprecated directive
grep -rn "@import" src --include="*.scss"Go through each hit and decide whether it imports a Sass module or is deliberate plain CSS passthrough.
2Count files that already use @use
grep -rln "@use" src --include="*.scss"Compare this list against the previous one to identify the modules still waiting to be migrated.
3Audit what each @extend actually reaches
grep -rn "@extend" src --include="*.scss"For every @extend you should be able to name the selectors it will hit.
4Find every !default variable
grep -rn "!default" src --include="*.scss"Confirm which variables are meant to be overridable from the outside.
5Spot deeply nested rules that duplicate output
grep -rn "^ *\(16,\)" src --include="*.scss"Excessive indentation is a reliable proxy for excessive nesting and selector combinations.
6Check whether hardcoded colours have converged into variables
grep -rn "#[0-9a-fA-F]\{3,6\}" src --include="*.scss"Literal colours scattered across files are a common reason theme switching stops working.
Command Examples
A parameterized mixin to centralize media queries
@mixin responsive($breakpoint) {
@media (max-width: $breakpoint) {
@content;
}
}
.card {
@include responsive(768px) {
display: block;
}
}With breakpoints scattered across a dozen files, changing one means a project-wide search and replace. Folding them into a parameterized mixin leaves the value in exactly one place, and every other rule calls @include. @content lets the caller own the body, so the mixin decides when the rules apply and the call site decides what they do.
Store design tokens in a map, import with @use under a namespace
$sizes: (small: 12px, large: 24px)
@use "variables" as vars
.btn {
font-size: vars.$sizes__small;
padding: map-get(vars.$sizes, small);
}The old @import leaks variables into the global scope, so a $primary defined in two files silently overwrites the other: the build passes but the result is not what you meant. @use namespaces every module, so collisions are just vars.$primary versus theme.$primary. A module is also evaluated once no matter how many times you @use it, while repeated @import re-emits all of its content.
Output
.btn {
font-size: 12px;
padding: 12px;
}Make defaults overridable with !default
$primary: #6366f1 !default
@use "config" with ($primary: red)
.btn { color: $primary }!default means "do not override if already defined": the right-hand value is taken only when $primary has no value yet. @use "config" with (...) injects configuration before the module is evaluated, which is exactly what this pattern needs. Without !default the consumer cannot change the theme colour and has to edit the source file.
Output
.btn {
color: red;
}@at-root escapes the current nesting scope
.theme-dark {
color: white;
@at-root .input {
border-color: #444;
}
}A .input nested inside .theme-dark compiles to .theme-dark .input (a descendant selector). If the intent was a global style that applies regardless of the theme scope, @at-root is required to lift it back to the top level. This is one of the easiest Sass semantics to get wrong and the hardest to spot by reading.
Output
.theme-dark {
color: white;
}
.input {
border-color: #444;
}Color functions to derive variants from one theme colour
$primary: #6366f1
.btn {
background: $primary;
color: white;
}
.btn:hover { background: darken($primary, 10%) }
.btn:disabled { background: lighten($primary, 10%) }Collapse the brand colour into a single $primary and derive hover, disabled, and tinted backgrounds from it with darken / lighten / mix / rgba, so re-theming touches one line. Note that rgba($color, $alpha) changes alpha, not the colour itself; use rgba($primary, 0.5) for a translucent tint and it composes against the colour as-is.
Output
.btn:hover {
background: #5046e0;
}Common Pitfalls
- Continuing to use @import instead of @use is the most common hazard: @import shares one global scope, so a duplicate $primary or @mixin silently overwrites the earlier one, the build still succeeds, and the shipped colours are simply wrong. Count the @import statements and convert shared variable files to @use "variables" as vars first.
- Using @import to load a file that contains @use fails to build, and once both module systems are mixed the error points at a completely unrelated file. On "This file is already loaded" or "Can't find stylesheet to import" errors, replace that file's @import with @use.
- darken / lighten / mix do numeric interpolation, not perceptual lightness adjustment. #6366f1 darkened by 10% does not look uniformly darker, and lighten on yellows is often invisible. When the design demands a clear contrast step, write an explicit hex value or use color.adjust with $lightness.
- Division: in Sass, 10px / 2 still yields 10px because division only applies in certain contexts, and a ratio needs math.div(10px, 2). In older versions / is a separator, so the mistake compiles cleanly, emits the original value, and the style silently does nothing.
- @error halts compilation while @warn only prints and continues. Putting @error in a shared mixin's parameter validation means one page passing a bad value fails the entire site build. Reserve it for cases that truly cannot degrade; use @warn everywhere else so the build survives.
- @extend merges selectors and widens the match, so extending a shared placeholder like %message-box from both .error and .warning can pull in elements you did not intend, and every new @extend forces a full recompile of the stylesheet. Prefer composing styles with a mixin.
Tips
- SCSS (.scss) is CSS-compatible, while Sass (.sass) uses indentation. SCSS is recommended.
- @use is Sass's modern module system and is clearer than the legacy @import.
- Use mixins to reuse code blocks; use % placeholder selectors to reuse styles without emitting duplicate CSS.
FAQ
Both @import and @use load files — which should new projects use?
Use @use. It is the recommended modular system and keeps namespaces explicit. After @use "variables" as vars you reference vars.$primary, while @use "variables" as * drops the prefix and matches the old behaviour. To override a module's defaults (the ones declared with !default), write @use "config" with ($primary: red). The older @import "variables" is legacy syntax with global scope and a real risk of name collisions — keep it out of new code. It still has one legitimate use: pulling in external CSS such as @import url("https://fonts.googleapis.com").
How should I divide work between mixins, @extend and % placeholders?
Mixins reuse a block of declarations and emit properties and values: define @mixin flex-center { display: flex; justify-content: center } and call it with @include flex-center. If callers need to supply their own rules, add @content and pass them in as in .btn { @include button { color: red } }. The % placeholder reuses a shared style group: %message-box { border: 1px solid } is inherited with @extend %message-box and never emits a selector of its own. Rule of thumb — properties go in a mixin, styles shared by a family of selectors go in a placeholder plus @extend.
How do I combine loops with interpolation to generate a family of classes?
The loop controls how many are produced and #{} splices the variable into the selector. @for $i from 1 through 3 { .col-#{$i} { } } yields .col-1 through .col-3; @each $color in red, blue, green { .#{$color} { } } iterates over values; @while $i > 0 { .item-#{$i} { } $i: $i - 1 } runs until the condition fails. Interpolation works in values too: content: "icon-#{$name}" emits a literal icon-xxx string instead of the raw #{$name} characters.
How do I avoid copy-pasting media queries for every breakpoint?
Turn the breakpoint into a parameterised mixin: @mixin responsive($breakpoint) { @media ... } defines the media condition once and callers pass the concrete value. Keep scale values in a map — $sizes: (small: 12px, large: 24px) read with map-get($sizes, small), extended with map-merge($sizes, (medium: 16px)). And when nesting gets too deep, @at-root emits the selector at the top level: .box { @at-root .inner { } } produces .inner rather than .box .inner.
How much do arithmetic and colour functions actually save in SCSS?
Numbers participate directly: width: 100px + 50px compiles to 150px, and helpers like @function px-to-rem($px) { @return $px / 16 * 1rem } package repeated math. On the colour side, darken(#6366f1, 10%), lighten(#6366f1, 10%) and mix(red, blue) derive shades without hand-rolled values, while rgba(#6366f1, 0.5) applies opacity to an existing brand colour. For diagnostics, @warn emits a warning and lets compilation continue, whereas @error aborts the build — handy for validating required mixin arguments.
Official References
Each command links to its official documentation below, so you can verify the latest usage and read deeper.
Maintained by LaoHand
Publicly updated on Jul 21, 2026, continuously proofread against official docs.
Contact Us
Wrong command or description? Send us corrections, business inquiries or product feedback by email.
Contact Us