Control Flow
|
This section documents Sass/SCSS as implemented by Dart Sass, the current official and actively maintained compiler — it is not tied to any specific book, build tool, or CSS framework (Bootstrap, Bulma, etc.). Unlike the other reference sections on this site, no single reference book underpins it: the content was generated with the assistance of AI from general knowledge of Dart Sass, and should be verified against the current official documentation at sass-lang.com before relying on it in production. Sass continues to evolve, so behaviour described here may lag the compiler you are actually running. This section’s bibliography lists the reference material consulted while preparing these pages. |
Control-flow directives let a stylesheet make decisions and generate rules programmatically. They run at compile time — the browser sees only the CSS they produced.
Note that all four directives can assign to variables in the enclosing scope directly, without !global (see
Variables), which is what makes accumulator patterns work.
@if / @else if / @else
@mixin theme-surface($theme) {
@if $theme == dark {
background: #121212;
color: #eeeeee;
} @else if $theme == light {
background: #ffffff;
color: #212121;
} @else {
@error "Unknown theme `#{$theme}`; expected `dark` or `light`.";
}
}
.panel--dark { @include theme-surface(dark); }
.panel--light { @include theme-surface(light); }
Putting @else on the same line as the closing brace (} @else) is a style convention, not a requirement — SCSS tolerates whitespace and comments between the two, so an @else on its own line compiles just as well.
Remember Sass’s truthiness rule: everything is truthy except false and null. 0, "", and () are all
truthy, unlike in JavaScript. Combined with the fact that map.get() returns null for a missing key, this
makes @if a natural guard:
@use "sass:map";
@mixin elevation($level, $config) {
$shadow: map.get($config, $level);
@if $shadow {
box-shadow: $shadow;
} @else {
@warn "No shadow defined for level `#{$level}`.";
}
}
The if() function
For choosing between two values rather than two blocks, the built-in if() function is more compact. Unlike
@if, it is an expression usable anywhere a value is expected, and it evaluates only the branch it returns:
@use "sass:color";
@function contrast($bg) {
@return if(color.channel($bg, "lightness", $space: hsl) > 55%, #212121, #ffffff);
}
$compact: false;
.badge {
background: #ffeb3b;
color: contrast(#ffeb3b); // #212121
padding: if($compact, 4px, 12px); // 12px
}
Current Dart Sass deprecates this comma-argument form of if() in favor of a modern CSS-style syntax
(if(sass($condition): $if-true; else: $if-false)) — see
the deprecation notice. The form above still works today and is what most
existing Sass code uses, but new code should prefer the CSS syntax.
@for
@for iterates over a numeric range. It comes in two forms, and the difference is the endpoint:
| Form | Range | @for $i from 1 through/to 3 |
|---|---|---|
|
Inclusive of the end value |
1, 2, 3 |
|
Exclusive of the end value |
1, 2 |
through is the one you usually want, since stylesheets tend to count from 1.
@use "sass:math";
// A 12-column grid
@for $i from 1 through 12 {
.col-#{$i} {
width: math.percentage(math.div($i, 12));
}
}
.col-1 { width: 8.3333333333%; }
.col-2 { width: 16.6666666667%; }
/* ... */
.col-12 { width: 100%; }
A spacing scale, using the loop variable in the value rather than the name:
$base: 4px;
@for $i from 1 through 6 {
.m-#{$i} { margin: $base * $i; }
.p-#{$i} { padding: $base * $i; }
}
Counting down works by swapping the bounds — Sass detects the direction automatically:
@for $i from 5 through 1 {
.z-#{$i} { z-index: $i * 10; } // emits .z-5 first, down to .z-1
}
Both bounds must be unitless integers (or numbers with compatible units); a non-integer is an error.
@each
@each iterates over the elements of a list or the entries of a map. It is the most useful of the four in
practice, because stylesheet data is usually a set of names rather than a numeric range.
Over a list
$sizes: xs, sm, md, lg, xl;
@each $size in $sizes {
.hidden-#{$size} { display: none; }
}
Over a map
Two variables destructure each entry into its key and value:
$theme-colours: (
primary: #3f51b5,
success: #43a047,
warning: #fb8c00,
danger: #e53935
);
@each $name, $colour in $theme-colours {
.text-#{$name} { color: $colour; }
.bg-#{$name} { background-color: $colour; }
.border-#{$name} { border-color: $colour; }
}
.text-primary { color: #3f51b5; }
.bg-primary { background-color: #3f51b5; }
.border-primary { border-color: #3f51b5; }
.text-success { color: #43a047; }
/* ... and so on */
This is the canonical utility-class generator: one source of truth for the palette, and three families of classes derived from it. Adding a colour to the map adds three classes automatically.
Destructuring a list of lists
The same multiple-variable syntax destructures nested lists, which is handy for tabular data:
$buttons:
(primary, #3f51b5, white),
(secondary, #757575, white),
(ghost, transparent, #3f51b5);
@each $name, $bg, $fg in $buttons {
.btn-#{$name} {
background: $bg;
color: $fg;
}
}
If an inner list is shorter than the variable list, the missing variables are null.
Nested @each
Combining two loops generates a matrix — useful for responsive utilities, though it multiplies output size quickly, so keep the input sets small:
@use "sass:map";
$breakpoints: (sm: 576px, md: 768px, lg: 992px);
$displays: none, block, flex, grid;
@each $bp-name, $bp-width in $breakpoints {
@media (min-width: $bp-width) {
@each $display in $displays {
.#{$bp-name}\:d-#{$display} { display: $display; }
}
}
}
@while
@while repeats as long as its condition holds. It is the rarest of the four — almost anything expressible
with @while is clearer as @for or @each — but it fits when the step is not a simple increment:
@use "sass:math";
// A modular type scale: each step is 1.25× the last, until it exceeds 48px
$size: 12px;
$step: 1;
@while $size < 48px {
.text-#{$step} { font-size: $size; }
$size: $size * 1.25;
$step: $step + 1;
}
.text-1 { font-size: 12px; }
.text-2 { font-size: 15px; }
.text-3 { font-size: 18.75px; }
.text-4 { font-size: 23.4375px; }
.text-5 { font-size: 29.296875px; }
.text-6 { font-size: 36.62109375px; }
.text-7 { font-size: 45.7763671875px; }
Make sure the condition can actually become false — a @while whose body never changes the variable it tests
will hang the compiler.
Choosing between them
-
@if— branch on a condition. Useif()when choosing between two values rather than two blocks. -
@each— iterate over named data. The default choice for generating classes from a list or map. -
@for— iterate over a numeric range, when the numbers themselves are meaningful (grid columns, spacing steps, z-index layers). -
@while— iterate until a condition changes, when the step isn’t a fixed increment.
A general caution: loops make it easy to emit a very large amount of CSS from a very small amount of source. A nested loop over three breakpoints and twenty utilities produces sixty rules — check the compiled output size when generating utility families.