TH The Sass Way
Sass & SCSS

Sass Maps Explained: @each Loops and Design Tokens in SCSS

Sass Maps Explained: @each Loops and Design Tokens in SCSS
tldrA Sass map is a data structure that stores key-value pairs inside parentheses, like $colors: ("primary": #0d6efd, "danger": #dc3545). You read values with map.get() from the built-in sass:map module and iterate over pairs with the @each rule to generate CSS automatically. Maps are the standard way to manage design tokens in SCSS — colors, breakpoints, and spacing scales live in one variable, and loops turn them into utility classes or CSS custom properties.

What Are Sass Maps?

A Sass map is a data structure that stores key-value pairs, written as comma-separated key: value entries inside parentheses. Maps let you group related values — colors, breakpoints, spacing scales — under one variable, read them with map.get(), and loop over them with @each to generate CSS automatically. If you have ever maintained a dozen loose variables like $color-primary, $color-secondary, and $color-danger, maps are the upgrade: one source of truth instead of a pile of near-duplicates.

Here is the simplest possible example:

$colors: (
  "primary": #0d6efd,
  "success": #198754,
  "danger": #dc3545
);

That single variable now holds three named values. Everything else in this guide — lookups, loops, nesting, design tokens — builds on this one idea.

Maps became widely used because they solve a real maintenance problem. When your theme colors live in a map, adding a fourth color is a one-line change, and every loop that generates classes from that map picks it up automatically. No hunting through the stylesheet for every place a color family is referenced.

Sass Map Syntax: The Rules That Matter

Map syntax is small, but a few details trip people up:

One more rule worth internalizing: maps are immutable. Functions like map.merge() never change the original map; they return a new one. If you want to "update" a map, reassign the variable to the returned result.

Reading Values with map.get

To read a value out of a map, load the built-in sass:map module and call map.get():

@use "sass:map";

$breakpoints: (
  "small": 576px,
  "medium": 768px,
  "large": 992px
);

.sidebar {
  width: map.get($breakpoints, "medium");
}

Compiled CSS:

.sidebar {
  width: 768px;
}

Two things to note. First, the @use "sass:map"; line is required in modern Dart Sass — the old global functions like map-get() still compile for now, but they are deprecated in favor of the namespaced module versions, the same way @import is deprecated in favor of @use. New code should use map.get(). Second, if the key does not exist, map.get() quietly returns null, and Sass silently drops any declaration whose value is null. That silent failure is convenient sometimes and maddening at other times, which is why the next function exists.

Guarding Lookups with map.has-key

map.has-key() returns true or false, which lets you fail loudly instead of silently:

@use "sass:map";

@function spacing($key) {
  @if not map.has-key($spacing, $key) {
    @error "Unknown spacing key: #{$key}.";
  }
  @return map.get($spacing, $key);
}

Wrapping lookups in a small function like this is one of the highest-value habits in Sass. A typo in a key becomes a clear compile-time error pointing at the bad name, instead of a missing property you discover in the browser three days later. If you are unsure whether logic like this belongs in a function or a mixin, our comparison of Sass mixins vs functions draws the line: functions return values, mixins output CSS.

Looping Through a Sass Map with @each

The @each rule is where maps earn their keep. It iterates over every pair in a map, binding the key and value to two variables:

$spacing: (
  "xs": 4px,
  "sm": 8px,
  "md": 16px,
  "lg": 32px
);

@each $name, $size in $spacing {
  .m-#{$name} {
    margin: $size;
  }
}

Compiled CSS:

.m-xs {
  margin: 4px;
}

.m-sm {
  margin: 8px;
}

.m-md {
  margin: 16px;
}

.m-lg {
  margin: 32px;
}

Four classes from four lines of loop. Add "xl": 48px to the map and you get .m-xl for free on the next compile.

The #{$name} syntax is interpolation, and it is mandatory here. Variables cannot appear directly inside selectors or property names — only inside values. Interpolation stamps the variable's text into the selector. Forgetting it is the single most common @each error, and the compiler message ("expected selector") does not always make the cause obvious.

The same pattern generates any utility family you want: text colors, font sizes, z-index layers. One map, one loop, and the generated CSS never drifts out of sync with the source values.

Nested Sass Maps

Because map values can themselves be maps, you can model structured data — a full theme, for instance:

@use "sass:map";

$theme: (
  "color": (
    "primary": #0d6efd,
    "danger": #dc3545
  ),
  "font": (
    "base": 16px,
    "heading": 32px
  )
);

Reading Nested Values

Modern Dart Sass lets map.get() take multiple keys and walk down the structure in one call:

.alert {
  color: map.get($theme, "color", "danger");
  font-size: map.get($theme, "font", "base");
}

Compiled CSS:

.alert {
  color: #dc3545;
  font-size: 16px;
}

If you are on an older Sass version, the equivalent is chaining: map.get(map.get($theme, "color"), "danger"). Same result, more noise — one more reason to keep your compiler current.

Looping Over Nested Maps

To iterate a nested map, nest the loops. The outer @each hands you each inner map, and the inner @each walks its pairs:

@each $category, $tokens in $theme {
  @each $token, $value in $tokens {
    // e.g. build class names like .color-primary, .font-base
  }
}

A word of caution from experience: two levels of nesting is the sweet spot. Three or more levels turns your stylesheet into a data-modeling exercise, and debugging a triple-nested loop is nobody's idea of a good afternoon. If your token structure is getting that deep, flatten it with compound keys like "color-primary" instead.

Design Tokens in SCSS: Maps Meet CSS Custom Properties

Design tokens — named values for color, spacing, typography, and radii that a whole team shares — map perfectly onto Sass maps. The most useful modern pattern combines both worlds: define tokens in a Sass map, then loop them out as CSS custom properties.

$tokens: (
  "color-primary": #0d6efd,
  "color-surface": #f8f9fa,
  "radius-md": 8px,
  "space-md": 16px
);

:root {
  @each $name, $value in $tokens {
    --#{$name}: #{$value};
  }
}

Compiled CSS:

:root {
  --color-primary: #0d6efd;
  --color-surface: #f8f9fa;
  --radius-md: 8px;
  --space-md: 16px;
}

Note the #{$value} interpolation on the value side. Custom property values are a special case in Sass: they are treated as verbatim text, so a bare $value would output the literal string $value rather than the color. Interpolation forces evaluation.

Why bother with both layers? Because they solve different problems. The Sass map gives you compile-time safety, loops, and a single editable source file. The custom properties give you runtime flexibility — theme switching, user preferences, values that JavaScript can read and change. Sass variables disappear at compile time; custom properties live on in the browser. Our guide to CSS custom properties covers that runtime side in depth, including how the two variable systems differ.

A Practical Pattern: Breakpoint Maps and a respond() Mixin

Here is the pattern that shows up in almost every serious SCSS codebase — a breakpoint map paired with a media-query mixin:

@use "sass:map";

$breakpoints: (
  "sm": 576px,
  "md": 768px,
  "lg": 992px,
  "xl": 1200px
);

@mixin respond($name) {
  @if map.has-key($breakpoints, $name) {
    @media (min-width: map.get($breakpoints, $name)) {
      @content;
    }
  } @else {
    @error "Unknown breakpoint: #{$name}.";
  }
}

.card {
  padding: 16px;

  @include respond("md") {
    padding: 32px;
  }
}

Compiled CSS:

.card {
  padding: 16px;
}

@media (min-width: 768px) {
  .card {
    padding: 32px;
  }
}

Every breakpoint in the project now lives in exactly one place, every media query is validated at compile time, and changing your md breakpoint is a one-character edit. This same map-plus-mixin architecture is how frameworks like Bootstrap organize their responsive systems internally.

Sass Map Functions Cheat Sheet

All of these live in the built-in sass:map module (@use "sass:map";):

Function What it does
map.get($map, $key...) Returns the value for a key (or nested keys); null if missing
map.has-key($map, $key...) Returns true if the key (or nested path) exists
map.keys($map) Returns all keys as a list
map.values($map) Returns all values as a list
map.merge($map1, $map2) Returns a new map combining both; $map2 wins on conflicts
map.deep-merge($map1, $map2) Like merge, but recurses into nested maps
map.set($map, $key..., $value) Returns a new map with one value changed
map.remove($map, $keys...) Returns a new map without the listed keys

Remember: every one of these returns a new map. map.merge($defaults, $overrides) is the standard way to build configurable components — define defaults in a map, let callers pass overrides, merge them, and read from the result.

Common Mistakes to Avoid

Where to Go Next

Maps and @each are the point where Sass stops being "CSS with variables" and becomes a small programming environment for your stylesheets. Once the pattern clicks — data in maps, CSS generated by loops — you will find uses for it everywhere: theme systems, spacing scales, utility classes, breakpoint management.

If you want to see how maps fit into the bigger picture alongside partials, mixins, and the module system, the complete guide to Sass walks the whole toolchain end to end. And when you build your first token system, resist the urge to model everything on day one. Start with colors and spacing in two small maps, loop them into custom properties, and grow the structure only when a real need shows up. The best token systems are boring — a short file anyone on the team can read, edit, and trust.

FAQ

How do I get a value from a Sass map?

Load the built-in module with @use "sass:map"; then call map.get($map, $key). For example, map.get($breakpoints, "medium") returns the value stored under that key. If the key does not exist, map.get() returns null rather than throwing an error, and Sass silently drops any declaration with a null value — so guard important lookups with map.has-key() when a typo should fail the build.

How do I loop through a Sass map?

Use the @each rule with two variables: @each $key, $value in $map { ... }. Inside the loop, use interpolation — #{$key} — anywhere the key appears in a selector or property name, because bare variables are only allowed in values. This pattern is the standard way to generate utility classes: a spacing map with four entries and a three-line loop produces four margin classes automatically.

What is the difference between map.get() and the old map-get()?

They do the same lookup, but map.get() is the modern, namespaced version from the sass:map module, loaded with @use "sass:map". The old global functions like map-get() and map-merge() still compile in Dart Sass but are deprecated, in the same wave of changes that replaced @import with @use. New code should use the module functions; they also gained extras like passing multiple keys to reach into nested maps.

How do nested Sass maps work?

A map value can itself be a map, which lets you model structured data like a theme with color and font groups. Modern Dart Sass reads nested values in one call by passing multiple keys: map.get($theme, "color", "danger"). To loop a nested map, nest two @each rules — the outer loop yields each inner map, the inner loop walks its pairs. Two levels of nesting is usually the practical limit before flattening becomes easier.

Should design tokens use Sass maps or CSS custom properties?

Use both, for different jobs. Define tokens in a Sass map so you get one editable source file, compile-time validation, and loops. Then output them as CSS custom properties with @each inside :root, interpolating the value as --#{$name}: #{$value}. The custom properties handle runtime needs like theme switching and JavaScript access, while the map keeps authoring centralized. Sass variables vanish at compile time; custom properties persist in the browser.

Can I change a value in a Sass map?

Not in place — Sass maps are immutable. Functions like map.set(), map.merge(), and map.remove() always return a new map and leave the original untouched, so you must assign the result back to a variable: $config: map.merge($defaults, $overrides);. That merge pattern is the idiomatic way to build configurable components, with $overrides winning wherever both maps define the same key.