Skip to main content
Frontend

Design Tokens: Name Colors by Purpose, Not by Hex

9 min read

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:

css

Step 2 — Map tokens to Tailwind utilities

Tailwind v4 uses @theme inline instead of tailwind.config.js:

css

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:

typescript

Step 4 — Use semantic classes in components

tsx

Common pitfalls

  • Hex or arbitrary values in JSX — breaks rebrand and dark mode.
  • Forgetting .dark overrides — light tokens leak into dark theme.
  • Using cn() to merge ad-hoc strings — use tv() 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.