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

- What Are Sass Maps?
- Sass Map Syntax: The Rules That Matter
- Reading Values with map.get
- Looping Through a Sass Map with @each
- Nested Sass Maps
- Design Tokens in SCSS: Maps Meet CSS Custom Properties
- A Practical Pattern: Breakpoint Maps and a respond() Mixin
- Sass Map Functions Cheat Sheet
- Common Mistakes to Avoid
- Where to Go Next
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:
- Parentheses are required.
$sizes: ("sm": 4px, "lg": 16px);works; dropping the parens does not create a map. - Keys can be any Sass value, but strings are the most common and the easiest to reason about. Quote them consistently — Sass treats
"md"andmdas equal strings, but mixing styles makes code harder to scan. - Values can be anything too, including other maps. That is what makes nested token structures possible.
- An empty map is
()— the same literal as an empty list. Sass treats it as both. - Trailing commas are allowed, which keeps diffs clean when you add entries.
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
- Forgetting interpolation in selectors.
.m-$namefails;.m-#{$name}works. - Forgetting interpolation in custom property values.
--x: $value;outputs literal text;--x: #{$value};outputs the value. - Expecting
map.get()to error on missing keys. It returnsnullsilently — guard withmap.has-key()or a wrapper function when a typo should fail the build. - Treating maps as mutable.
map.set()does nothing unless you assign its return value. - Over-nesting. Two levels of structure is plenty; flatten deeper hierarchies into compound keys.
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.