Semantic tokens
Tokens named by purpose (--color-surface) not value (--gray-900). Swap the token set to switch themes — components never change.
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.