A Theming System with Maps + CSS Custom Properties

Marry Sass maps (build-time structure) with CSS custom properties (runtime switching) for real themes.

Here is the single most important idea in modern Sass architecture: Sass maps give you structure at build time; CSS custom properties give you switching at runtime. Use both. Sass loops over your token map and emits custom properties; your components read those custom properties, never the Sass variables directly.

Why not Sass variables alone?

Sass variables vanish at compile time, so a .dark class cannot change them in the browser. Custom properties cascade and can be overridden per-scope — which is exactly what a theme toggle needs.

:root { --brand: #CC6699; }
.dark { --brand: #E29CBC; }
.btn  { background: var(--brand); } // follows the theme live

The pattern

  1. Define themes as Sass maps (structured, validated).
  2. Loop with @each to emit --custom-props per theme scope.
  3. Components consume only var(--token) — theme-agnostic by construction.

Example

Example · css
@use 'sass:map';

// 1) Themes as structured maps
$themes: (
  'light': ('bg': #ffffff, 'fg': #111827, 'brand': #CC6699),
  'dark':  ('bg': #0b0f19, 'fg': #e5e7eb, 'brand': #E29CBC),
);

// 2) Emit custom properties per theme scope
@each $name, $tokens in $themes {
  $selector: if($name == 'light', ':root', '[data-theme="#{$name}"]');
  #{$selector} {
    @each $key, $value in $tokens {
      --#{$key}: #{$value};
    }
  }
}

// 3) Components read var() only — theme-agnostic
body { background: var(--bg); color: var(--fg); }
.btn {
  background: var(--brand, #{map.get(map.get($themes, 'light'), 'brand')});
  color: #fff;
}
// Toggle by setting data-theme="dark" on <html> — no recompile.

When to use it

  • A SaaS dashboard stores both light and dark design tokens in Sass maps and emits them as CSS custom properties so a data-theme attribute switch requires zero JavaScript style manipulation.
  • A multi-tenant platform generates a separate compiled CSS file per brand by swapping a single Sass token map and recompiling, enabling true white-labelling without runtime overhead.
  • A developer co-locates a component's dark-mode overrides right next to its base styles by wrapping them in a theme() mixin that emits under the correct [data-theme] selector.

More examples

Emit theme tokens as custom properties

A single mixin loops over any theme map and emits every key as a CSS custom property, keeping the switch to dark mode a one-attribute change.

Example · css
@use 'sass:map';

$light: (bg: #ffffff, text: #111827, surface: #f9fafb, border: #e5e7eb);
$dark:  (bg: #0f172a, text: #f1f5f9, surface: #1e293b, border: #334155);

@mixin emit-theme($map) {
  @each $k, $v in $map { --#{$k}: #{$v}; }
}

:root             { @include emit-theme($light); }
[data-theme=dark] { @include emit-theme($dark);  }

Component referencing theme tokens

The component uses CSS custom properties from the theme system so it automatically reflects the active theme with no extra CSS.

Example · css
.card {
  background: var(--surface);
  color:      var(--text);
  border:     1px solid var(--border);
  padding:    1.5rem;
  transition: background 0.2s ease, color 0.2s ease;
}

Per-brand theme compilation

Each brand entry file passes a different token map to the shared theme module, producing independent compiled files for white-labelling.

Example · css
// brand-red/main.scss
$theme: (primary: #dc2626, bg: #fff5f5, text: #1a1a1a);
@use '../shared/theme-system' with ($tokens: $theme);

// brand-blue/main.scss
$theme: (primary: #2563eb, bg: #eff6ff, text: #1e3a5f);
@use '../shared/theme-system' with ($tokens: $theme);
// Each compiles to a separate .css file.

Discussion

  • Be the first to comment on this lesson.