Home / Snippets / Layout /

Semantic tokens

Tokens named by purpose (--color-surface) not value (--gray-900). Swap the token set to switch themes — components never change.

--color-backgroundpage background
--color-surfacecard / panel
--color-on-surfaceprimary text
--color-mutedsecondary text
--color-primaryinteractive / accent
--color-borderdividers / outlines
Widely Supported
layouttokensno-js

Quick implementation

/* Semantic tokens — layer 2 over raw color primitives.
   Change these to change the theme everywhere. */
:root {
  /* Surfaces */
  --color-background:    oklch(0.98 0.005 250);
  --color-surface:       oklch(1    0     0);
  --color-surface-raised:oklch(0.97 0.005 250);

  /* Text */
  --color-on-background: oklch(0.15 0.02 260);
  --color-on-surface:    oklch(0.15 0.02 260);
  --color-muted:         oklch(0.50 0.02 260);

  /* Interactive */
  --color-primary:       oklch(0.52 0.22 265);
  --color-on-primary:    oklch(0.98 0    0);
  --color-primary-hover: oklch(0.45 0.22 265);

  /* Borders */
  --color-border:        oklch(0.88 0.01 260);
  --color-border-strong: oklch(0.75 0.02 260);

  /* Feedback */
  --color-success:       oklch(0.52 0.18 145);
  --color-warning:       oklch(0.65 0.18 75);
  --color-danger:        oklch(0.55 0.22 25);
}

/* Dark theme — same tokens, different values */
@media (prefers-color-scheme: dark) {
  :root {
    --color-background:    oklch(0.13 0.02 260);
    --color-surface:       oklch(0.19 0.02 260);
    --color-surface-raised:oklch(0.22 0.02 260);
    --color-on-background: oklch(0.93 0.01 260);
    --color-on-surface:    oklch(0.93 0.01 260);
    --color-muted:         oklch(0.63 0.02 260);
    --color-border:        oklch(0.28 0.02 260);
    --color-border-strong: oklch(0.38 0.03 260);
  }
}

Prompt this to your LLM

Includes role, constraints, framework variants, and edge cases.

You are a senior frontend engineer building a two-layer token
system for a design system.

Layer 1 — primitives: raw color values like --blue-500
Layer 2 — semantic tokens: purpose-named aliases like --color-primary

Goal: Create a semantic token layer that covers:
- Surfaces: background, surface, surface-raised, overlay
- Text: on-background, on-surface, muted, on-primary
- Interactive: primary, on-primary, primary-hover, primary-focus-ring
- Borders: border, border-strong
- Feedback: success, warning, danger (each with on-* pair)

Constraints:
- Use oklch() colors for perceptually uniform manipulation
- Provide both light and dark values using prefers-color-scheme
- Add [data-theme="dark"] selector as a JS-toggled fallback
  alongside the media query
- Show how a .card component uses only semantic tokens, never
  primitive values directly

Return only the CSS with inline comments.

Primitive vs semantic token layers

A two-layer token system separates "what color values exist" (primitives: --blue-500: oklch(0.52 0.22 265)) from "what those colors mean" (semantic: --color-primary: var(--blue-500)). Components reference only semantic tokens. When you want a dark mode or a different brand color, you update the semantic layer to point to different primitives — every component updates without touching a single component file. Using raw values directly in components (background: oklch(0.52 0.22 265)) creates a maintenance problem at scale.

Naming: role-state, not color-value

Good semantic token names describe the role (--color-surface), not the value (--color-gray-100). A token named --color-gray-100 forces you to think about what value it contains when reading component code. A token named --color-surface tells you the intent — it's the background of an elevated surface. When the design switches from gray to warm beige surfaces, the semantic name stays correct; a gray-named token becomes a lie. The Material Design and Radix UI token systems both use this role-naming convention.