Semantic Tokens for Light and Dark UI Switching

Semantic tokens bridge the gap between design intent and implementation by abstracting raw style values into purpose-driven roles. By establishing an intermediary layer that defines what an element is rather than what it looks like, teams can automate the transition between light and dark user interfaces. This article explains the architectural shift from static colors to contextual tokens, demonstrates how CSS variables facilitate theme switching at runtime, and highlights the workflow efficiencies gained through semantic abstraction.

Understanding Semantic Tokens

Design tokens typically exist in a three-tier hierarchy: primitive, semantic, and component-specific.

  1. Primitive Tokens (Global): These represent raw design decisions with no contextual meaning, such as color-blue-500: #3b82f6 or color-neutral-100: #f3f4f6.
  2. Semantic Tokens (Alias): These map primitive values to specific design intents or roles within an interface, such as background-surface-default, text-primary, or border-critical.
  3. Component Tokens: These apply semantic tokens to specific elements, such as button-primary-background.

When interfaces rely directly on primitive tokens, switching themes requires overriding individual values across every component stylesheet. Semantic tokens eliminate this friction by acting as a universal contract: the component requests a role (for example, text-primary), and the design system resolves that role to the appropriate primitive value based on the active theme.

Decoupling Roles from Visual Values

The core mechanism of theme automation is the decoupling of visual attributes from functional contexts. In a standard UI:

  • Light Mode: background-canvas resolves to neutral-50 (#ffffff), while text-body resolves to neutral-900 (#111827).
  • Dark Mode: background-canvas maps to neutral-950 (#030712), while text-body maps to neutral-100 (#f3f4f6).

Because the UI components only consume the semantic reference (background-canvas), the component logic itself remains completely agnostic to the theme being rendered. There is no need for conditional CSS rules, duplicate classes, or JavaScript-driven class toggling at the component level.

Implementing Dynamic Token Resolution

The most efficient way to automate theme switching in the browser is through CSS Custom Properties mapped to root or data attributes.

In this architecture, primitive values are declared at the base level, and semantic aliases are defined inside specific theme scopes:

:root {
  /* Primitive Tokens */
  --color-white: #ffffff;
  --color-gray-900: #111827;
  --color-gray-100: #f3f4f6;
  --color-gray-800: #1f2937;

  /* Semantic Mappings for Default (Light) Mode */
  --surface-primary: var(--color-white);
  --surface-secondary: var(--color-gray-100);
  --text-primary: var(--color-gray-900);
}

/* Semantic Overrides for Dark Mode */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --surface-primary: var(--color-gray-900);
    --surface-secondary: var(--color-gray-800);
    --text-primary: var(--color-white);
  }
}

Components simply consume the semantic variable:

.card {
  background-color: var(--surface-primary);
  color: var(--text-primary);
}

When a user toggles the UI mode, the application only updates a single attribute—such as setting data-theme="dark" on the <html> or <body> element—or relies automatically on the system's prefers-color-scheme query. The browser updates all instances instantaneously via the CSS cascade.

Scaling Across Design and Development

Semantic tokens also streamline collaboration between designers and engineers. Modern design platforms (like Figma Variables) allow designers to define modes matching the exact naming conventions used in the codebase. When a design changes from light to dark in the design tool, it mirrors the exact variable swapping occurring in production.

This shared taxonomy reduces the overhead of maintaining theme parity, guarantees consistent contrast ratios across modes, and allows entire applications to support dark mode out of the box with zero component-level overrides.