Design Tokens and Theming
Name colors by role, not by value, and reskin an entire app by swapping a few variables.
There are two kinds of tokens, and mixing them up is why themes get painful.
Primitive vs semantic tokens
- Primitive: raw palette values —
--color-cyan-500. Neutral, meaningless on their own. - Semantic: tokens named by role —
--color-surface,--color-muted,--color-danger. These point at primitives.
Markup should speak in semantic tokens (bg-surface, text-muted). Then a theme is just a different mapping of roles to values.
Retheming without touching HTML
Define the roles in @theme, and override them under a .dark (or .brand-x) scope. Not a single class in your components changes:
@theme {
--color-surface: white;
--color-ink: #0f172a;
}
.dark {
--color-surface: #1e293b;
--color-ink: #f1f5f9;
}Every bg-surface and text-ink in the app flips at once. That is the whole promise of tokens: intent lives in the markup, values live in one place.
Example
When to use it
- A product redesign replaces every hardcoded color in a Tailwind config by mapping semantic tokens like --color-brand and --color-surface, so a single CSS variable swap re-themes the whole app.
- A SaaS platform ships a dark mode by switching a data-theme attribute on the root element, and every component automatically re-colors because all classes reference semantic CSS variables instead of raw palette values.
- A design system team publishes primitive tokens (raw hex values) in one file and semantic tokens (role-named aliases) in another, keeping components decoupled from the underlying palette.
More examples
Primitive tokens in CSS variables
Separates primitive (raw value) tokens from semantic (role-named) tokens so components reference meaning, not specific hues, enabling painless re-theming.
:root {
/* Primitive β raw palette */
--color-cyan-400: #22d3ee;
--color-cyan-600: #0891b2;
--color-gray-50: #f9fafb;
--color-gray-900: #111827;
/* Semantic β role-named aliases */
--color-brand: var(--color-cyan-600);
--color-brand-hover: var(--color-cyan-400);
--color-surface: var(--color-gray-50);
--color-on-surface: var(--color-gray-900);
}Wiring tokens into Tailwind v4 theme
In Tailwind v4, @theme wires CSS custom properties into the design system so bg-brand and text-text become valid utility classes that resolve through the token chain.
/* app.css β Tailwind v4 entry point */
@import "tailwindcss";
@theme {
--color-brand: var(--color-cyan-600);
--color-brand-light: var(--color-cyan-400);
--color-surface: var(--color-gray-50);
--color-text: var(--color-gray-900);
}Dark mode via data-theme token swap
Swapping data-theme="dark" on the root element redefines only the semantic tokens; every component using bg-surface or text-text re-colors automatically with no class changes.
:root {
--color-brand: #0891b2;
--color-surface: #f9fafb;
--color-text: #111827;
}
[data-theme="dark"] {
--color-brand: #22d3ee;
--color-surface: #111827;
--color-text: #f9fafb;
}
Discussion