Introduction to Control Directives
Control directives are one of the most powerful features of Sass, bringing programming logic to your stylesheets. They allow you to make decisions, create repetitive structures, and iterate through collections—capabilities that simply don't exist in regular CSS.
Analogy: Control Directives as a Factory Assembly Line
Think of control directives as the decision-making systems in a factory assembly line. Conditionals (@if/@else) are like quality control checkpoints that direct products down different paths based on specific criteria. Loops (@for, @each, @while) are like automated machines that perform the same operation on a series of identical parts, but can adjust settings for each individual piece. Together, they transform your Sass from a static blueprint into a dynamic production system that can respond to different conditions and efficiently handle repetitive tasks.
The main control directives in Sass are:
- @if/@else - Conditional branching
- @for - Loop a specific number of times
- @each - Loop through items in a list or map
- @while - Loop while a condition is true
Conditional Directives: @if, @else if, and @else
The @if, @else if, and @else directives allow you to conditionally
include CSS based on some condition. This is incredibly useful for creating flexible mixins,
adaptive styles, and theme systems.
Basic Syntax
@mixin text-contrast($background) {
@if (lightness($background) > 50%) {
color: #000000; // Dark text for light backgrounds
} @else {
color: #ffffff; // Light text for dark backgrounds
}
}
.button-primary {
background-color: #007bff;
@include text-contrast(#007bff);
}
.button-light {
background-color: #e9ecef;
@include text-contrast(#e9ecef);
}
Multiple Conditions with @else if
@mixin button-variant($color) {
@if $color == 'primary' {
background-color: #007bff;
border-color: #007bff;
color: white;
} @else if $color == 'success' {
background-color: #28a745;
border-color: #28a745;
color: white;
} @else if $color == 'danger' {
background-color: #dc3545;
border-color: #dc3545;
color: white;
} @else {
// Default/fallback styling
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
}
.btn-primary {
@include button-variant('primary');
}
.btn-success {
@include button-variant('success');
}
.btn-danger {
@include button-variant('danger');
}
.btn-default {
@include button-variant('default');
}
Complex Conditions
Sass supports logical operators, allowing for complex conditions:
@mixin responsive-property($property, $value, $screen-size) {
// Screen size presets
$is-small: $screen-size == 'small' or $screen-size <= 576px;
$is-medium: $screen-size == 'medium' or ($screen-size > 576px and $screen-size <= 992px);
$is-large: $screen-size == 'large' or $screen-size > 992px;
@if $is-small {
#{$property}: $value * 0.8; // Smaller value for small screens
} @else if $is-medium {
#{$property}: $value; // Base value for medium screens
} @else if $is-large {
#{$property}: $value * 1.2; // Larger value for large screens
} @else {
// Fallback if no match
#{$property}: $value;
}
}
.hero-title {
@include responsive-property('font-size', 2rem, 'small');
@media (min-width: 576px) {
@include responsive-property('font-size', 2rem, 'medium');
}
@media (min-width: 992px) {
@include responsive-property('font-size', 2rem, 'large');
}
}
Nesting Conditionals
You can nest conditional directives for more complex logic:
@mixin configure-card($variant, $is-dark-mode) {
border-radius: 8px;
@if $variant == 'elevated' {
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
@if $is-dark-mode {
background-color: #2c2c2c;
color: white;
} @else {
background-color: white;
color: #333;
}
} @else if $variant == 'outlined' {
border: 1px solid;
@if $is-dark-mode {
border-color: #666;
background-color: transparent;
color: #ddd;
} @else {
border-color: #ddd;
background-color: transparent;
color: #333;
}
}
}
// Light mode cards
.card-elevated {
@include configure-card('elevated', false);
}
.card-outlined {
@include configure-card('outlined', false);
}
// Dark mode theme
.dark-theme {
.card-elevated {
@include configure-card('elevated', true);
}
.card-outlined {
@include configure-card('outlined', true);
}
}
Conditional @content in Mixins
You can use conditionals to determine whether or not to include @content in a mixin:
@mixin when-dark-theme($enabled: true) {
@if $enabled {
@media (prefers-color-scheme: dark) {
@content;
}
}
}
.button {
background-color: #f0f0f0;
color: #333;
@include when-dark-theme {
background-color: #333;
color: #f0f0f0;
}
}
The @for Loop
The @for directive allows you to generate repeated styles with variations.
It iterates a counter variable over a range of values.
Basic @for Syntax
There are two forms of @for loops:
@for from ... through ...
Includes the final number
@for $i from 1 through 5 {
.item-#{$i} {
width: 20% * $i;
}
}
/* Compiles to:
.item-1 { width: 20%; }
.item-2 { width: 40%; }
.item-3 { width: 60%; }
.item-4 { width: 80%; }
.item-5 { width: 100%; }
*/
@for from ... to ...
Excludes the final number
@for $i from 1 to 5 {
.item-#{$i} {
width: 20% * $i;
}
}
/* Compiles to:
.item-1 { width: 20%; }
.item-2 { width: 40%; }
.item-3 { width: 60%; }
.item-4 { width: 80%; }
*/
Creating a Grid System
One common application of @for loops is creating a grid system:
$columns: 12;
// Generate column classes
@for $i from 1 through $columns {
.col-#{$i} {
width: percentage($i / $columns);
}
}
// Generate offset classes
@for $i from 0 through ($columns - 1) {
.offset-#{$i} {
margin-left: percentage($i / $columns);
}
}
Generating Utility Classes
@for loops are perfect for creating utility classes that follow a numerical pattern:
// Generate margin utilities
@for $i from 0 through 9 {
.m-#{$i} {
margin: $i * 0.25rem;
}
.mt-#{$i} {
margin-top: $i * 0.25rem;
}
.mr-#{$i} {
margin-right: $i * 0.25rem;
}
.mb-#{$i} {
margin-bottom: $i * 0.25rem;
}
.ml-#{$i} {
margin-left: $i * 0.25rem;
}
}
// Generate z-index utilities
@for $i from 1 through 10 {
.z-#{$i * 10} {
z-index: $i * 10;
}
}
Creating Visual Effects
@for loops can also create complex visual effects:
// Create a staggered animation delay for list items
@for $i from 1 through 10 {
.animate-list li:nth-child(#{$i}) {
animation-delay: 0.1s * $i;
}
}
// Create a stepped gradient
.gradient-steps {
background: linear-gradient(to right,
@for $i from 0 through 10 {
rgba(0, 0, 255, $i / 10) #{$i * 10}%#{if($i < 10, ',', '')}
}
);
}
The @each Loop
The @each directive allows you to iterate through a list or map,
making it perfect for generating variations of a component using a set of predefined values.
Basic @each with Lists
$colors: 'red', 'green', 'blue', 'yellow';
@each $color in $colors {
.text-#{$color} {
color: #{$color};
}
}
/* Compiles to:
.text-red { color: red; }
.text-green { color: green; }
.text-blue { color: blue; }
.text-yellow { color: yellow; }
*/
@each with Maps
Maps let you associate values with keys, making your loops more meaningful:
$theme-colors: (
'primary': #007bff,
'success': #28a745,
'danger': #dc3545,
'warning': #ffc107
);
@each $name, $color in $theme-colors {
.btn-#{$name} {
background-color: $color;
border-color: darken($color, 10%);
&:hover {
background-color: darken($color, 7.5%);
border-color: darken($color, 15%);
}
}
.text-#{$name} {
color: $color;
}
.bg-#{$name} {
background-color: $color;
}
}
Nested Maps for Component Variations
For more complex components, you can use nested maps:
$alert-variants: (
'info': (
'background': #e6f7ff,
'border': #91d5ff,
'text': #0066cc
),
'success': (
'background': #e6ffee,
'border': #8eedac,
'text': #00a854
),
'warning': (
'background': #fffbe6,
'border': #ffe58f,
'text': #d48806
),
'danger': (
'background': #fff1f0,
'border': #ffa39e,
'text': #cf1322
)
);
@each $variant, $properties in $alert-variants {
.alert-#{$variant} {
padding: 15px;
border-radius: 4px;
background-color: map-get($properties, 'background');
border: 1px solid map-get($properties, 'border');
color: map-get($properties, 'text');
}
}
Multiple Variables in @each
@each can also work with lists of lists, allowing you to assign multiple variables at once:
$button-styles: (
'primary' #007bff white,
'success' #28a745 white,
'danger' #dc3545 white,
'light' #f8f9fa #212529
);
@each $name, $background, $text in $button-styles {
.btn-#{$name} {
background-color: $background;
color: $text;
&:hover {
background-color: darken($background, 7.5%);
}
}
}
This is conceptually similar to destructuring in JavaScript or other languages.
@each for Managing Assets
@each is useful for managing asset paths and creating icon systems:
$social-icons: 'facebook', 'twitter', 'instagram', 'linkedin', 'youtube';
@each $platform in $social-icons {
.icon-#{$platform} {
background-image: url('/assets/icons/#{$platform}.svg');
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
}
The @while Loop
The @while directive creates a loop that repeats until a condition becomes false.
It's less commonly used than @for and @each, but can be powerful
for situations where you need more complex stopping conditions.
Basic @while Syntax
$i: 1;
@while $i <= 5 {
.item-#{$i} {
width: 20% * $i;
}
$i: $i + 1;
}
This example produces the same output as the @for example we saw earlier, but with a different control structure.
Creating a Fibonacci Sequence
@while is useful for more complex series where the next value depends on previous values:
// Generate a Fibonacci sequence for spacing
$fib1: 1;
$fib2: 1;
$count: 1;
@while $count <= 10 {
.space-#{$count} {
margin-bottom: $fib1 + rem;
}
$fib3: $fib1 + $fib2;
$fib1: $fib2;
$fib2: $fib3;
$count: $count + 1;
}
/* Compiles to:
.space-1 { margin-bottom: 1rem; }
.space-2 { margin-bottom: 1rem; }
.space-3 { margin-bottom: 2rem; }
.space-4 { margin-bottom: 3rem; }
.space-5 { margin-bottom: 5rem; }
.space-6 { margin-bottom: 8rem; }
.space-7 { margin-bottom: 13rem; }
.space-8 { margin-bottom: 21rem; }
.space-9 { margin-bottom: 34rem; }
.space-10 { margin-bottom: 55rem; }
*/
Creating Color Variations
@while can generate color variations until a threshold is reached:
// Generate progressively darker variations of a color
$base-color: #3498db;
$variation: 1;
$darkness: 0;
@while $darkness < 50 {
.color-variation-#{$variation} {
background-color: darken($base-color, $darkness);
}
$darkness: $darkness + 10;
$variation: $variation + 1;
}
/* Compiles to:
.color-variation-1 { background-color: #3498db; } /* 0% darker */
.color-variation-2 { background-color: #2589ce; } /* 10% darker */
.color-variation-3 { background-color: #1a7ab9; } /* 20% darker */
.color-variation-4 { background-color: #166aa3; } /* 30% darker */
.color-variation-5 { background-color: #125a8d; } /* 40% darker */
.color-variation-6 { background-color: #0e4b76; } /* 50% darker */
*/
Real-World Applications
Creating a Complete Utility Class System
Popular frameworks like Tailwind CSS use control directives to generate thousands of utility classes:
// _utilities.scss
// Configuration
$spacer: 0.25rem;
$spacings: (0, 1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 20, 24, 32, 40, 48, 56, 64);
$sides: ('t': 'top', 'r': 'right', 'b': 'bottom', 'l': 'left');
$breakpoints: (
'sm': 640px,
'md': 768px,
'lg': 1024px,
'xl': 1280px,
'2xl': 1536px
);
$colors: (
'blue': #3498db,
'green': #2ecc71,
'red': #e74c3c,
'yellow': #f1c40f,
'purple': #9b59b6,
'gray': #95a5a6,
'black': #000,
'white': #fff
);
// Generate responsive utilities
@mixin responsive-variants {
@content;
@each $breakpoint, $value in $breakpoints {
@media (min-width: $value) {
.#{$breakpoint}\: {
@content;
}
}
}
}
// Spacing utilities (margin and padding)
@include responsive-variants {
// All sides
@each $space in $spacings {
.m-#{$space} {
margin: $spacer * $space;
}
.p-#{$space} {
padding: $spacer * $space;
}
}
// Individual sides
@each $abbr, $side in $sides {
@each $space in $spacings {
.m#{$abbr}-#{$space} {
margin-#{$side}: $spacer * $space;
}
.p#{$abbr}-#{$space} {
padding-#{$side}: $spacer * $space;
}
}
}
// X and Y sides
@each $space in $spacings {
.mx-#{$space} {
margin-left: $spacer * $space;
margin-right: $spacer * $space;
}
.my-#{$space} {
margin-top: $spacer * $space;
margin-bottom: $spacer * $space;
}
.px-#{$space} {
padding-left: $spacer * $space;
padding-right: $spacer * $space;
}
.py-#{$space} {
padding-top: $spacer * $space;
padding-bottom: $spacer * $space;
}
}
}
// Width and height utilities
@include responsive-variants {
@for $i from 1 through 6 {
.w-#{$i*10} {
width: $i * 10%;
}
.h-#{$i*10} {
height: $i * 10%;
}
}
.w-full {
width: 100%;
}
.h-full {
height: 100%;
}
.w-screen {
width: 100vw;
}
.h-screen {
height: 100vh;
}
}
// Color utilities
@include responsive-variants {
@each $name, $value in $colors {
.text-#{$name} {
color: $value;
}
.bg-#{$name} {
background-color: $value;
}
.border-#{$name} {
border-color: $value;
}
}
}
Theme System with Material Design Color Palette
Control directives can create a sophisticated theme system:
// _theme.scss
// Material Design color palette
$material-colors: (
'red': (
50: #ffebee,
100: #ffcdd2,
200: #ef9a9a,
300: #e57373,
400: #ef5350,
500: #f44336,
600: #e53935,
700: #d32f2f,
800: #c62828,
900: #b71c1c,
'accent': #ff8a80
),
'blue': (
50: #e3f2fd,
100: #bbdefb,
200: #90caf9,
300: #64b5f6,
400: #42a5f5,
500: #2196f3,
600: #1e88e5,
700: #1976d2,
800: #1565c0,
900: #0d47a1,
'accent': #82b1ff
),
'green': (
50: #e8f5e9,
100: #c8e6c9,
200: #a5d6a7,
300: #81c784,
400: #66bb6a,
500: #4caf50,
600: #43a047,
700: #388e3c,
800: #2e7d32,
900: #1b5e20,
'accent': #b9f6ca
)
);
// Theme configuration
$themes: (
'light': (
'background': white,
'surface': #f5f5f5,
'text-primary': rgba(0, 0, 0, 0.87),
'text-secondary': rgba(0, 0, 0, 0.6),
'primary-color': map-get(map-get($material-colors, 'blue'), 500),
'secondary-color': map-get(map-get($material-colors, 'green'), 500),
'error-color': map-get(map-get($material-colors, 'red'), 500)
),
'dark': (
'background': #121212,
'surface': #1e1e1e,
'text-primary': rgba(255, 255, 255, 0.87),
'text-secondary': rgba(255, 255, 255, 0.6),
'primary-color': map-get(map-get($material-colors, 'blue'), 300),
'secondary-color': map-get(map-get($material-colors, 'green'), 300),
'error-color': map-get(map-get($material-colors, 'red'), 300)
)
);
// Material color palette generation
@each $color-name, $color-variants in $material-colors {
@each $variant, $value in $color-variants {
.#{$color-name}-#{$variant} {
background-color: $value;
}
.text-#{$color-name}-#{$variant} {
color: $value;
}
}
}
// Theme generation
@each $theme-name, $theme-vars in $themes {
.theme-#{$theme-name} {
// Theme variables for the entire app
--background: #{map-get($theme-vars, 'background')};
--surface: #{map-get($theme-vars, 'surface')};
--text-primary: #{map-get($theme-vars, 'text-primary')};
--text-secondary: #{map-get($theme-vars, 'text-secondary')};
--primary-color: #{map-get($theme-vars, 'primary-color')};
--secondary-color: #{map-get($theme-vars, 'secondary-color')};
--error-color: #{map-get($theme-vars, 'error-color')};
// Apply base theme styles
background-color: var(--background);
color: var(--text-primary);
// Component theming examples
.app-bar {
background-color: var(--primary-color);
color: white;
}
.card {
background-color: var(--surface);
color: var(--text-primary);
}
.button-primary {
background-color: var(--primary-color);
color: white;
}
.button-secondary {
background-color: var(--secondary-color);
color: white;
}
.error-message {
color: var(--error-color);
}
}
}
Responsive Typography System
Create a complete responsive typography system with a few directives:
// _typography.scss
// Type scale base sizes for different breakpoints
$type-scales: (
'base': (
'xs': 0.75rem, // 12px
'sm': 0.875rem, // 14px
'base': 1rem, // 16px
'lg': 1.125rem, // 18px
'xl': 1.25rem, // 20px
'2xl': 1.5rem, // 24px
'3xl': 1.875rem, // 30px
'4xl': 2.25rem, // 36px
'5xl': 3rem, // 48px
'6xl': 4rem // 64px
),
'md': (
'xs': 0.75rem,
'sm': 0.875rem,
'base': 1rem,
'lg': 1.125rem,
'xl': 1.25rem,
'2xl': 1.5rem,
'3xl': 2rem, // Slightly larger
'4xl': 2.5rem, // Slightly larger
'5xl': 3.25rem, // Slightly larger
'6xl': 4.5rem // Slightly larger
),
'lg': (
'xs': 0.75rem,
'sm': 0.875rem,
'base': 1rem,
'lg': 1.125rem,
'xl': 1.25rem,
'2xl': 1.5rem,
'3xl': 2.25rem, // Even larger
'4xl': 2.75rem, // Even larger
'5xl': 3.5rem, // Even larger
'6xl': 5rem // Even larger
)
);
// Line heights for different text sizes
$line-heights: (
'xs': 1.5,
'sm': 1.5,
'base': 1.5,
'lg': 1.5,
'xl': 1.4,
'2xl': 1.35,
'3xl': 1.3,
'4xl': 1.25,
'5xl': 1.2,
'6xl': 1.1
);
// Font weights
$font-weights: (
'thin': 100,
'extralight': 200,
'light': 300,
'normal': 400,
'medium': 500,
'semibold': 600,
'bold': 700,
'extrabold': 800,
'black': 900
);
// Generate base typography styles
@each $size, $value in map-get($type-scales, 'base') {
.text-#{$size} {
font-size: $value;
line-height: map-get($line-heights, $size);
}
}
// Generate font weight utilities
@each $weight-name, $weight-value in $font-weights {
.font-#{$weight-name} {
font-weight: $weight-value;
}
}
// Generate responsive typography
@each $breakpoint-name, $scale in $type-scales {
@if $breakpoint-name == 'base' {
// Base styles already generated
} @else if $breakpoint-name == 'md' {
@media (min-width: 768px) {
@each $size, $value in $scale {
.text-#{$size} {
font-size: $value;
}
}
}
} @else if $breakpoint-name == 'lg' {
@media (min-width: 1024px) {
@each $size, $value in $scale {
.text-#{$size} {
font-size: $value;
}
}
}
}
}
// Generate responsive alignment utilities
@each $alignment in (left, center, right, justify) {
.text-#{$alignment} {
text-align: $alignment;
}
@media (min-width: 768px) {
.md\:text-#{$alignment} {
text-align: $alignment;
}
}
@media (min-width: 1024px) {
.lg\:text-#{$alignment} {
text-align: $alignment;
}
}
}
Best Practices for Control Directives
Keep Logic Simple
While Sass control directives give you programming power, complex logic can become difficult to maintain:
// Avoid overly complex conditional logic
@mixin complex-component($type, $size, $theme, $state) {
@if $type == 'button' {
@if $theme == 'light' {
@if $state == 'active' {
// Deeply nested conditions become hard to follow
} @else {
// ...
}
} @else if $theme == 'dark' {
// ...
}
} @else if $type == 'card' {
// ...
}
}
// Instead, break logic into smaller, focused mixins
@mixin button-base {
// Common button styles
}
@mixin button-theme($theme) {
@if $theme == 'light' {
// Light theme styles
} @else if $theme == 'dark' {
// Dark theme styles
}
}
@mixin button-state($state) {
@if $state == 'active' {
// Active state styles
} @else if $state == 'disabled' {
// Disabled state styles
}
}
Use Maps for Complex Data
Maps are more maintainable than hard-coded values in loops:
// Less maintainable approach
@for $i from 1 through 6 {
.h#{$i} {
@if $i == 1 {
font-size: 2.5rem;
} @else if $i == 2 {
font-size: 2rem;
} @else if $i == 3 {
font-size: 1.75rem;
} @else if $i == 4 {
font-size: 1.5rem;
} @else if $i == 5 {
font-size: 1.25rem;
} @else if $i == 6 {
font-size: 1rem;
}
}
}
// More maintainable with maps
$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;
}
}
Avoid Too Many Generated Variations
Be strategic about which variations to generate to avoid CSS bloat:
// This could generate thousands of classes - too many!
@for $r from 0 through 255 {
@for $g from 0 through 255 {
@for $b from 0 through 255 {
.color-#{$r}-#{$g}-#{$b} {
color: rgb($r, $g, $b);
}
}
}
}
// Better approach: generate only the ones you'll actually use
$color-values: (0, 51, 102, 153, 204, 255);
@each $r in $color-values {
@each $g in $color-values {
@each $b in $color-values {
.color-#{$r}-#{$g}-#{$b} {
color: rgb($r, $g, $b);
}
}
}
}
Document Complex Control Flow
Add comments to explain what your control directives are doing, especially for complex logic:
// Generate a stepped color progression from one color to another
// $steps: Number of color variations to create
// $start: Starting color (hex)
// $end: Ending color (hex)
@mixin color-progression($prefix, $start, $end, $steps) {
// Store the red, green, and blue components of each color
$start-red: red($start);
$start-green: green($start);
$start-blue: blue($start);
$end-red: red($end);
$end-green: green($end);
$end-blue: blue($end);
// Calculate the amount to change each component per step
$step-red: ($end-red - $start-red) / ($steps - 1);
$step-green: ($end-green - $start-green) / ($steps - 1);
$step-blue: ($end-blue - $start-blue) / ($steps - 1);
// Generate each step
@for $i from 0 through ($steps - 1) {
$current-red: round($start-red + ($step-red * $i));
$current-green: round($start-green + ($step-green * $i));
$current-blue: round($start-blue + ($step-blue * $i));
.#{$prefix}-#{$i + 1} {
background-color: rgb($current-red, $current-green, $current-blue);
}
}
}
Practice Activities
- Responsive Grid System: Create a responsive grid system with 12 columns using @for loops. Include classes for different breakpoints (e.g., .col-sm-6, .col-md-4).
- Dynamic Theme Builder: Build a theme system that generates light and dark themes for a UI component library. Use @each to iterate through components and @if to handle theme-specific variations.
- Utility Class Generator: Create a set of utility classes for spacing (margin and padding) using @for loops. Include variations for different sides (top, right, bottom, left) and sizes.
- Social Media Icon System: Build a social media icon system using @each to iterate through a list of platforms, generating classes with appropriate colors and background images.
- Advanced Typography Scale: Create a responsive typography system with a modular scale. Use @each and @if directives to handle different breakpoints and ensure text remains readable at all screen sizes.
Additional Resources
- Sass Control Directives Documentation
- For and While Loops in Sass on CSS-Tricks
- Sass Loops: For, Each, and While
- Tailwind CSS GitHub - See how a real utility framework uses loops
- Material Design Color System - Good reference for implementing color systems with Sass
Key Takeaways
- Control directives bring programming logic to your stylesheets, allowing for dynamic CSS generation
- @if/@else enables conditional styling based on variables or computed values
- @for creates numerical iterations, perfect for grid systems and stepped variations
- @each iterates through lists and maps, ideal for generating variations from predefined values
- @while allows more complex stopping conditions for specialized use cases
- Control directives are key to building design systems, utility frameworks, and theming engines
- Best practices include keeping logic simple, using maps for data, and avoiding CSS bloat