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

Try it yourself
Loading editor…
Press Run to see the result.

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.

Example Β· css
: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.

Example Β· css
/* 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.

Example Β· css
:root {
  --color-brand:   #0891b2;
  --color-surface: #f9fafb;
  --color-text:    #111827;
}

[data-theme="dark"] {
  --color-brand:   #22d3ee;
  --color-surface: #111827;
  --color-text:    #f9fafb;
}

Discussion

  • Be the first to comment on this lesson.
Design Tokens and Theming β€” Tailwind CSS | SoundsCode