Design Tokens: Name Colors by Purpose, Not by Hex
Hard-coding #2563EB on every button works until marketing picks a new brand color. Then you grep the entire repo.
A design token is a named value — primary, muted-foreground, surface-panel — shared between designers and developers. When the brand changes, you update the token definition once.
Vocabulary
| Layer | Example | Who owns it |
|---|---|---|
| Primitive | --color-twilight-ink |
Design system |
| Semantic | --primary, --background |
Design system |
| Utility class | bg-primary, text-overcast |
Tailwind / CSS framework |
| Component variant | variant: 'primary' |
Component author |
Steps
Step 1 — Define primitives and semantics in CSS
Light and dark themes live in the same file. Primitives are raw colors; semantics map intent:
Step 2 — Map tokens to Tailwind utilities
Tailwind v4 uses @theme inline instead of tailwind.config.js:
Now bg-primary and text-muted-foreground resolve through your token layer.
Step 3 — Compose variants with tailwind-variants
Group conditional classes in one place — never scatter strings across JSX:
Step 4 — Use semantic classes in components
Common pitfalls
- Hex or arbitrary values in JSX — breaks rebrand and dark mode.
- Forgetting
.darkoverrides — light tokens leak into dark theme. - Using
cn()to merge ad-hoc strings — usetv()variants instead.
Verify it works
Toggle dark mode. Change --primary in CSS. Every button and link should update without touching component files.
Takeaway
Name colors by role, wire through CSS variables, expose as utilities, compose with tv(). Rebrands become a token-file change, not a repo-wide search.