name: theming-design-tokens description: Builds design-token systems with CSS custom properties — primitive/semantic/component tiers, dark mode via prefers-color-scheme with a data-theme override, spacing/radius/shadow scales, and contrast-safe color palettes. Use when the user asks to set up design tokens or CSS variables, add dark mode or theme switching, define a color palette or spacing scale, or make theme colors meet contrast requirements. Do not use for typeface selection and type scales (choosing-typography) or for one-off page styling that no system will reuse.
Theming with Design Tokens
When to use / when NOT to use
- Use for: token architecture, palettes, dark mode/theming, spacing/radius/shadow scales, migrating hard-coded values to tokens.
- Do NOT use for: font pairing and type scales (→ choosing-typography); layout mechanics (→ designing-responsive-layouts); styling a single element nothing else will reuse — just style it.
Core rules
-
Three tiers, referenced downward only: primitive → semantic → component. Components consume semantic tokens; semantic tokens reference primitives; nothing skips upward.
- ✅
--color-blue-600: #2563eb;→--color-accent: var(--color-blue-600);→--button-bg: var(--color-accent); - ❌
--button-bg: #2563eb;(component pinned to a raw hex)
- ✅
-
Semantic names describe role, never appearance.
- ✅
--color-bg-surface,--color-text-muted,--color-border-danger - ❌
--light-gray-2,--dark-blue-bg(breaks the moment the theme flips)
- ✅
-
Dark mode = redefining semantic tokens only. Primitives and components never change per theme.
-
Both mechanisms, always:
@media (prefers-color-scheme: dark)for the default,[data-theme="dark"]/[data-theme="light"]for the user override; pluscolor-scheme: light darkon:rootso form controls and scrollbars follow. -
Contrast is enforced in the token definitions, not per usage: every
text/bgsemantic pair ≥ 4.5:1 (WCAG AA), every UI-component/border pair ≥ 3:1. Verify pairs when defining them — then usage is safe by construction. -
Scales, not ad-hoc values. Spacing on a base-4/8 scale (
0.25/0.5/0.75/1/1.5/2/3/4rem), 3–4 radii, 3 shadow levels. A new value must join the scale or justify itself.- ✅
padding: var(--space-4) var(--space-6); - ❌
padding: 13px 22px;
- ✅
-
Token count stays small: ~10–20 semantic color tokens covers most products. If you have 60, roles are duplicated — merge before adding.
Workflow
- Define primitives: a 3–5 step neutral ramp + 1 accent ramp (+ success/warn/danger primitives), spacing/radius/shadow scales.
- Define semantic tokens for both themes (rule 2–4), checking each text/bg pair's contrast ratio as you go (rule 5).
- Wire components to semantic tokens only.
- Add the theme toggle contract:
data-themeattribute on<html>, persisted; absence = follow system. - Validate: toggle both themes; confirm every visible pairing passes 4.5:1 (text) / 3:1 (UI); grep the stylesheet for stray hex/rgb values outside the primitives block — there should be none.
Edge cases & failure modes
- Brand color fails contrast on light bg: keep the brand primitive for large/decorative use; add a darkened
-text-safevariant for text/small UI, and note the pair it passes against. - Images/illustrations in dark mode: don't invert; reduce brightness slightly (
filter: brightness(0.9)) or provide themed assets. - Shadows invisible on dark surfaces: in dark themes, convey elevation with a subtly lighter surface token instead of larger shadows.
- Third-party widgets ignore tokens: scope overrides in one
@layer third-partyblock rather than scattering!important. - Flash of wrong theme on load: set
data-themein a tiny inline script in<head>before CSS paints.
References
Full starter token sheet (light + dark), toggle script, and contrast-checked palette: see references/patterns.md