π Control Directives and Loops
Plain CSS can't make a decision or repeat itself. Sass can. Control directives β @if, @for, @each, and @while β turn your stylesheet from a static blueprint into a small program that generates CSS. This is the machinery behind every utility framework, grid system, and theming engine you'll ever use.
π― Learning Objectives
By the end of this lesson, you will be able to:
- Branch styles conditionally with
@if,@else if, and@else - Generate numbered classes with
@forand know the difference betweenthroughandto - Iterate lists and maps with
@each, including multi-value and nested-map forms - Use
@whilefor loops whose stopping condition isn't a simple count - Apply these to build a grid, spacing utilities, and a theme system β and know when generation becomes bloat
Estimated Time: 35β45 minutes β’ Difficulty: Intermediate
Hands-on: Generate a full set of margin utility classes from a single loop.
In This Lesson
Why Logic in a Stylesheet?
Control directives bring genuine programming constructs β decisions and repetition β into your Sass. Instead of hand-writing forty nearly identical spacing classes, you describe the pattern once and let Sass expand it.
π‘ Analogy β a factory assembly line. Conditionals (@if/@else) are the quality-control checkpoints that send a product down one path or another. Loops (@for,@each,@while) are the machines that repeat one operation across many parts, adjusting a setting each time. Together they turn a static blueprint into a dynamic production line.
Sass gives you four control directives:
@if/@else if/@elseβ conditional branching@forβ loop a set number of times over a numeric range@eachβ loop over the items of a list or the pairs of a map@whileβ loop as long as a condition stays true
Conditionals: @if / @else
The @if family includes or excludes CSS based on a condition. It shines inside mixins, where the same mixin can adapt its output to its arguments.
Basic branching
Here a mixin picks readable text color based on a background's lightness. Modern Dart Sass exposes lightness() through the sass:color module:
@use "sass:color";
@mixin text-contrast($background) {
@if color.lightness($background) > 50% {
color: #000; // dark text on a light background
} @else {
color: #fff; // light text on a dark background
}
}
.button-primary { background: #007bff; @include text-contrast(#007bff); }
.button-light { background: #e9ecef; @include text-contrast(#e9ecef); }
Multiple branches with @else if
@mixin button-variant($kind) {
@if $kind == "primary" {
background: #007bff; border-color: #007bff; color: #fff;
} @else if $kind == "success" {
background: #28a745; border-color: #28a745; color: #fff;
} @else if $kind == "danger" {
background: #dc3545; border-color: #dc3545; color: #fff;
} @else {
background: #6c757d; border-color: #6c757d; color: #fff; // fallback
}
}
.btn-primary { @include button-variant("primary"); }
.btn-success { @include button-variant("success"); }
.btn-danger { @include button-variant("danger"); }
Sass supports the logical operators and, or, and not, so conditions can be as expressive as you need. Conditionals can also gate whether a mixin emits its @content block β handy for optional wrappers like a dark-mode media query.
π Truthiness in Sass
Only false and null are falsy in Sass. Every other value β including 0, an empty string, and an empty list β is truthy. This trips up developers coming from JavaScript or Python.
The @for Loop
@for iterates a counter over a numeric range. It has two forms that differ only in whether the final number is included:
| Form | Range | 1 β¦ 5 yields |
|---|---|---|
@for $i from 1 through 5 | Inclusive of the end | 1, 2, 3, 4, 5 |
@for $i from 1 to 5 | Exclusive of the end | 1, 2, 3, 4 |
A classic use is generating a grid. Modern Sass does division through the sass:math module (the old $i / $columns syntax is deprecated):
@use "sass:math";
$columns: 12;
@for $i from 1 through $columns {
.col-#{$i} {
width: math.percentage(math.div($i, $columns));
}
}
// .col-1 { width: 8.3333%; } β¦ .col-12 { width: 100%; }
The #{$i} syntax is interpolation β it drops a variable's value into a selector name or property. It's what makes generated class names like .col-7 possible.
@for is also perfect for stepped effects, such as staggering an animation delay down a list:
@for $i from 1 through 10 {
.animate-list li:nth-child(#{$i}) {
animation-delay: 0.1s * $i;
}
}
The @each Loop
@each walks through a list or a map. It's the most-used loop in real codebases because most design tokens live in lists and maps.
Over a list
$colors: red, green, blue, yellow;
@each $color in $colors {
.text-#{$color} { color: $color; }
}
// .text-red { color: red; } β¦ .text-yellow { color: yellow; }
Over a map (key + value)
Maps pair a name with a value, which reads far better than a bare list. Access is via the sass:map module in modern Sass:
@use "sass:color";
$theme-colors: (
"primary": #007bff,
"success": #28a745,
"danger": #dc3545,
"warning": #ffc107,
);
@each $name, $value in $theme-colors {
.btn-#{$name} {
background-color: $value;
border-color: color.adjust($value, $lightness: -10%);
&:hover { background-color: color.adjust($value, $lightness: -7.5%); }
}
.text-#{$name} { color: $value; }
.bg-#{$name} { background-color: $value; }
}
Destructuring multiple values
If each list item is itself a small list, @each can unpack several variables at once β conceptually like destructuring in JavaScript:
$button-styles:
("primary" #007bff white),
("success" #28a745 white),
("light" #f8f9fa #212529);
@each $name, $bg, $text in $button-styles {
.btn-#{$name} { background-color: $bg; color: $text; }
}
β Maps are the workhorse
When you see a framework generate dozens of themed classes, it's almost always @each over a map of design tokens. Change the map, and every generated class updates β a single source of truth for your color system.
The @while Loop
@while repeats as long as its condition holds. It's the least-used loop because @for and @each cover most needs β but it's the right tool when the next value depends on previous values, or when the stopping condition isn't a straight count.
Basic form
$i: 1;
@while $i <= 5 {
.item-#{$i} { width: 20% * $i; }
$i: $i + 1; // you must advance the counter yourself
}
Note the manual increment β forget it and you create an infinite loop that hangs the compiler. That footgun is exactly why @for is preferred for simple counts.
When @while earns its place: a Fibonacci spacing scale
Here each step depends on the two before it, which a counter-based @for can't express cleanly:
$a: 1;
$b: 1;
$n: 1;
@while $n <= 8 {
.space-#{$n} { margin-bottom: #{$a}rem; }
$next: $a + $b;
$a: $b;
$b: $next;
$n: $n + 1;
}
// .space-1 { 1rem } .space-2 { 1rem } .space-3 { 2rem } .space-4 { 3rem } .space-5 { 5rem } β¦
β οΈ Guard against runaway loops
Every @while must contain a statement that eventually makes its condition false. If it doesn't, compilation never finishes. When in doubt, reach for @for or @each, which can't loop forever.
Real-World: Utilities & Themes
Combining loops and maps is how real design systems are built. Two patterns dominate.
A spacing utility scale
Utility frameworks like Tailwind generate thousands of classes this way. A map of steps, a loop over sides, and you have a complete spacing system:
$spacer: 0.25rem;
$sizes: (0, 1, 2, 3, 4, 6, 8, 12);
$sides: ("t": "top", "r": "right", "b": "bottom", "l": "left");
@each $side-key, $side-name in $sides {
@each $size in $sizes {
.m#{$side-key}-#{$size} { margin-#{$side-name}: $spacer * $size; }
.p#{$side-key}-#{$size} { padding-#{$side-name}: $spacer * $size; }
}
}
// .mt-4 { margin-top: 1rem; } .pl-2 { padding-left: 0.5rem; } β¦ and so on
A theme system driven by a nested map
Loop over a map of themes to emit a class per theme, each setting CSS custom properties your components then read at runtime:
@use "sass:map";
$themes: (
"light": ("bg": #ffffff, "text": #333333, "primary": #0066cc),
"dark": ("bg": #121212, "text": #f5f5f5, "primary": #4d9fff),
);
@each $name, $vars in $themes {
.theme-#{$name} {
--bg: #{map.get($vars, "bg")};
--text: #{map.get($vars, "text")};
--primary: #{map.get($vars, "primary")};
background-color: var(--bg);
color: var(--text);
}
}
This hybrid β Sass loops to generate the rules, CSS custom properties to apply them β is the modern standard, because custom properties can switch themes live in the browser without recompiling.
Best Practices
β Do
- Store data in maps, then loop β one source of truth beats scattered
@ifladders. - Keep conditional logic shallow; split deeply nested
@ifs into small focused mixins. - Comment non-obvious loops so the next reader understands what's being generated.
β οΈ Don't
- Generate variations you'll never use β a triple-nested RGB loop can emit millions of classes and megabytes of CSS.
- Bury business logic in five levels of nested conditionals; it becomes unreadable and unmaintainable.
- Forget the counter update in a
@whileloop.
Prefer a map + @each over a long @if/@else if chain. Compare:
// Harder to maintain β a conditional ladder
@for $i from 1 through 6 {
.h#{$i} {
@if $i == 1 { font-size: 2.5rem; }
@else if $i == 2 { font-size: 2rem; }
// β¦four more branchesβ¦
}
}
// Cleaner β data in a map
$heading-sizes: (1: 2.5rem, 2: 2rem, 3: 1.75rem, 4: 1.5rem, 5: 1.25rem, 6: 1rem);
@each $level, $size in $heading-sizes {
.h#{$level} { font-size: $size; }
}
Hands-on Exercise
ποΈ Generate a Margin Utility Scale
Objective: Use a single loop to generate an all-sides margin utility scale, then extend it to individual sides.
Instructions:
- Define
$spacer: 0.25rem;and a list$steps: (0, 1, 2, 3, 4, 5);. - With
@each, generate.m-0 β¦ .m-5where.m-Nsetsmargin: $spacer * N. - Stretch: add a nested loop over a
$sidesmap to also emit.mt-N,.mr-N,.mb-N,.ml-N. - Predict the CSS produced for
.mb-3.
π‘ Hint
.mb-3 should be margin-bottom: 0.75rem; because 0.25rem * 3 = 0.75rem. Use interpolation #{$key} for the side abbreviation and #{$name} for the CSS property.
β Solution
$spacer: 0.25rem;
$steps: (0, 1, 2, 3, 4, 5);
$sides: ("t": "top", "r": "right", "b": "bottom", "l": "left");
// All-sides margins
@each $n in $steps {
.m-#{$n} { margin: $spacer * $n; }
}
// Individual sides
@each $key, $name in $sides {
@each $n in $steps {
.m#{$key}-#{$n} { margin-#{$name}: $spacer * $n; }
}
}
.mb-3 compiles to:
.mb-3 { margin-bottom: 0.75rem; }
Six steps Γ four sides plus the all-sides set = 30 utility classes from about ten lines of Sass.
π― Quick Quiz
Question 1: What does @for $i from 1 to 5 produce for $i?
Question 2: Which loop is the natural choice for iterating a map of theme colors (name β value)?
Question 3: What's the danger unique to a @while loop?
Summary & Quiz
π Key Takeaways
- Control directives add decisions and repetition to CSS, letting you generate rules from patterns.
@if/@elsebranch on conditions; remember onlyfalseandnullare falsy.@forcounts a numeric range βthroughincludes the end,toexcludes it.@eachiterates lists and maps and is the workhorse of design-token systems.@whilehandles condition-based loops but risks running forever β advance the counter.- Prefer maps + loops over long conditional ladders, and don't generate classes you'll never use.
π Further Reading
- Sass docs β flow control rules
- CSS-Tricks β For and While loops in Sass
- Sass docs β maps and the sass:map module
π What's Next?
You can now generate CSS programmatically. The next question is where all this code should live. In Sass Architecture and Organization you'll learn folder patterns like 7-1 and ITCSS, the modern @use/@forward module system, and how to keep a large stylesheet sane.
π Great work!
You've unlocked the loops and logic that power every serious CSS framework.