# Semantic Design Tokens: Architecting Scalable Theming with CSS Variables

> Deep dive into semantic design tokens and CSS variables for scalable theming: token tiers, dark mode, multi-brand support, and the pitfalls of flat token architectures.

- **Published:** 2025-04-14
- **Author:** Clixo
- **Reading time:** 5 min read
- **Tags:** design tokens, css variables, theming, design systems
- **Canonical URL:** https://clixo.sh/blog/semantic-design-tokens-scalable-theming-css-variables

You shipped dark mode in a weekend — by wrapping the entire app in a CSS class and overriding a hundred color values one by one. It works, mostly. But now a second brand is on the roadmap, and the question of how you handle that without duplicating the entire theme is one you do not have a clean answer to. The architecture you need is a semantic token layer backed by CSS custom properties.

## The Problem with Flat Token Systems

A flat token system maps names directly to values: `--color-blue: #1A73E8`. Engineers use `--color-blue` wherever they need that color. This is better than hardcoding hex values, but only marginally. When you need a dark theme, you are still swapping raw color values. When you need a second brand, you are rewriting every reference. The semantic layer is what transforms token systems from a color list into a theming architecture.

## Semantic Design Tokens: The Core Concept

Semantic tokens express **intent**, not value. Instead of `--color-blue`, you have `--color-action-primary`. The name communicates the role. The value can change — to a different blue, to a green, to whatever the dark variant requires — without changing any component code that references the token.

The architecture works in two tiers:

```mermaid
flowchart LR
  P["Primitive Tokens\n--palette-blue-500"] --> S["Semantic Tokens\n--color-action-primary"]
  S --> C1["Button component"]
  S --> C2["Link component"]
  S --> C3["Icon component"]
  T["Theme selector\ndata-theme=dark"] --> S
  B["Brand selector\ndata-brand=alpha"] --> S
```


**Primitive tokens** hold raw values and live at the `:root` level. They are your complete palette.

```css
:root {
  --palette-blue-500: #1a73e8;
  --palette-blue-600: #1557b0;
  --palette-neutral-50: #f8f9fa;
  --palette-neutral-900: #1a1a1a;
}
```

**Semantic tokens** reference primitives and express roles.

```css
:root {
  --color-action-primary: var(--palette-blue-500);
  --color-action-primary-hover: var(--palette-blue-600);
  --color-surface-default: var(--palette-neutral-50);
  --color-text-default: var(--palette-neutral-900);
}
```

Components consume only semantic tokens. A button's background uses `var(--color-action-primary)`, never `var(--palette-blue-500)`.

## How Theming Works at the Semantic Layer

Dark mode does not require touching a single component. You redefine semantic tokens within a theme selector:

```css
[data-theme="dark"] {
  --color-surface-default: var(--palette-neutral-900);
  --color-text-default: var(--palette-neutral-50);
  --color-action-primary: var(--palette-blue-400);
}
```

The button still reads `var(--color-action-primary)`. The value it resolves to has changed. No component code changed.

This pattern extends cleanly to system preferences:

```css
@media (prefers-color-scheme: dark) {
  :root {
    --color-surface-default: var(--palette-neutral-900);
    --color-text-default: var(--palette-neutral-50);
  }
}
```

## Multi-Brand Theming with Semantic Tokens

Multi-brand theming follows the same pattern. Each brand defines its own primitive palette and remaps semantic tokens to those primitives. Component code is unchanged.

```css
/* Brand A */
[data-brand="alpha"] {
  --palette-brand-primary: #0057b8;
  --palette-brand-secondary: #ff6b00;
  --color-action-primary: var(--palette-brand-primary);
}

/* Brand B */
[data-brand="beta"] {
  --palette-brand-primary: #28a745;
  --palette-brand-secondary: #6c757d;
  --color-action-primary: var(--palette-brand-primary);
}
```

Switch the `data-brand` attribute at the root and the entire product re-skins. This is the architecture that makes white-labeling manageable rather than a full re-engineering effort.

## Organizing Semantic Tokens at Scale

As the token system grows, organization matters. Group semantic tokens by category, not by component:

- **Surface tokens** — backgrounds, cards, overlays
- **Text tokens** — default, secondary, disabled, on-brand
- **Action tokens** — primary, secondary, destructive, ghost
- **Feedback tokens** — error, warning, success, info
- **Border tokens** — default, focus, error
- **Elevation tokens** — shadow scales

Avoid creating a new semantic token for every component. `--button-background-primary` and `--input-border-color` seem specific, but they usually duplicate what `--color-action-primary` and `--color-border-default` already express. Add component-level tokens only when a component genuinely diverges from semantic defaults in ways that need to be overrideable.

## Token Generation and Build Tooling

Manually maintaining CSS custom properties alongside JSON or YAML token sources is error-prone. Use a build tool to generate platform outputs from a single source.

Style Dictionary is the most widely adopted option. You define tokens once in JSON:

```json
{
  "color": {
    "action": {
      "primary": { "value": "{palette.blue.500}" }
    }
  }
}
```

Style Dictionary outputs CSS custom properties, JavaScript objects, Swift color definitions, or Android XML from the same source. The token source is version-controlled and becomes the single source of truth that Figma variables, web CSS, and native platforms all derive from.

## The Cascade Is Your Friend

One advantage of CSS custom properties over preprocessor variables (Sass, Less) is runtime mutability and cascade support. You can override a token at any level of the DOM tree:

```css
.high-contrast-region {
  --color-text-default: #000000;
  --color-surface-default: #ffffff;
}
```

This enables granular accessibility accommodations — a high-contrast section within a standard-contrast page — without a separate theme selector at the root.

## Common Architectural Mistakes

**Using primitives in component code.** The moment an engineer writes `var(--palette-blue-500)` in a component file, that component is invisible to the theming system.

**Semantic tokens that reference other semantic tokens.** Chains of variable references add complexity without benefit. Semantic tokens should resolve to primitives, not to other semantic tokens.

**Creating semantic tokens before knowing their consumer.** Tokens defined speculatively without a component consuming them tend to accumulate and fragment the namespace. Build tokens in response to real component needs.

**Neglecting contrast audits at the semantic layer.** Document which semantic token combinations meet WCAG contrast thresholds, and recheck after every theme change. A dark theme that passes in design often fails in contrast testing.

## What This Architecture Enables

A semantic token system backed by CSS custom properties is what makes all the expensive operations cheap: dark mode becomes a configuration file, rebranding becomes a token remap, and accessibility themes become overrides. The component library stays stable as the visual layer evolves around it.

[Clixo architects token systems and theming layers for product teams building across multiple surfaces and brands.](https://clixo.sh/#contact)

---

Clixo · 1141 W Bryn Mawr Ave, Itasca, IL 60143, US · [hello@clixo.sh](mailto:hello@clixo.sh)
[Start a build](https://clixo.sh/#contact) · [All services](https://clixo.sh/services) · [Agent guide (llms.txt)](https://clixo.sh/llms.txt)
