Skip to main content

πŸ” 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 @for and know the difference between through and to
  • Iterate lists and maps with @each, including multi-value and nested-map forms
  • Use @while for 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
flowchart TB A[Control Directives] --> B["@if / @else"] A --> C["@for"] A --> D["@each"] A --> E["@while"] B --> F[Conditional logic] C --> G[Numeric iteration] D --> H[Collection iteration] E --> I[Condition-based iteration]

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:

FormRange1 … 5 yields
@for $i from 1 through 5Inclusive of the end1, 2, 3, 4, 5
@for $i from 1 to 5Exclusive of the end1, 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 @if ladders.
  • 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 @while loop.

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:

  1. Define $spacer: 0.25rem; and a list $steps: (0, 1, 2, 3, 4, 5);.
  2. With @each, generate .m-0 … .m-5 where .m-N sets margin: $spacer * N.
  3. Stretch: add a nested loop over a $sides map to also emit .mt-N, .mr-N, .mb-N, .ml-N.
  4. 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/@else branch on conditions; remember only false and null are falsy.
  • @for counts a numeric range β€” through includes the end, to excludes it.
  • @each iterates lists and maps and is the workhorse of design-token systems.
  • @while handles 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

πŸš€ 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.