Post 15

Design Tokens That Survive a Rebrand

A token layer is only worth building if it makes the next change cheap. Most do the opposite — they hardcode today's brand into a thousand component files. Here is how to structure tokens, in CSS, so a rebrand is an afternoon.

Apr 14, 2026/10 min readDesign Systems
ShareY
Design Tokens That Survive a Rebrand

Every design system has tokens. Far fewer have tokens that actually help. The test is simple: when the brand changes — new primary colour, tighter spacing scale, different corner radius — how many files do you touch? If the answer is more than one, the token layer is decoration.

Three tiers, not one

The mistake is a single flat list: --blue-500, --gray-100, --space-4. That is a palette, not a token system. A system that survives change has three tiers:

  • Primitive — the raw values. --blue-500: oklch(0.62 0.19 255). These rarely change and mean nothing on their own.
  • Semantic — what a value is *for*. --color-primary, --color-surface, --color-border. These reference primitives and are what components actually use.
  • Component (optional) — overrides for one component that genuinely needs to deviate. --btn-primary-bg. Use sparingly; every one is a small debt.
tokens/primitives.css
1:root {2  /* raw scale — semantic tokens point here, components never do */3  --green-500: oklch(0.68 0.19 145);4  --green-600: oklch(0.60 0.20 145);5  --zinc-50:  oklch(0.98 0    0);6  --zinc-500: oklch(0.55 0.01 286);7  --zinc-900: oklch(0.21 0.01 286);8  --zinc-950: oklch(0.14 0.01 286);9}
tokens/semantic.css — the only layer components touch
1:root {2  --color-background: var(--zinc-50);3  --color-foreground: var(--zinc-950);4  --color-muted:      var(--zinc-500);5  --color-primary:    var(--green-600);6  --color-primary-foreground: var(--zinc-50);7  --color-border: color-mix(in oklch, var(--color-foreground) 12%, transparent);8 9  --radius: 0.5rem;10  --radius-sm: calc(var(--radius) - 4px);11  --radius-lg: calc(var(--radius) + 4px);12}

A rebrand now touches the semantic layer only. Components were never named --green-600, so they do not care that it is now --indigo-600.

Name by role, never by appearance

--color-danger, not --color-red. --color-accent, not --color-purple. The moment a token name describes how something looks rather than what it does, it is a landmine for the next designer who wants danger to be orange.

Bad (appearance)Good (role)
--text-dark--color-foreground
--bg-light-gray--color-muted
--border-thin--border-width-hairline
--blue-link--color-link

Theme by swapping the semantic layer

Dark mode, high contrast, a white-label client theme — these are all the same operation: redefine the semantic tokens, leave primitives and components untouched.

tokens/themes.css
1.dark {2  --color-background: var(--zinc-950);3  --color-foreground: var(--zinc-50);4  --color-primary: var(--green-500);   /* brighter green reads better on dark */5}6 7[data-theme="contrast"] {8  --color-foreground: #000;9  --color-border: #000;10  --color-muted: #1a1a1a;11}12 13[data-brand="acme"] {14  --color-primary: oklch(0.55 0.22 25); /* client's orange — one line */15}
Tip

Because tokens cascade, a themed section can be nested anywhere. Wrap a preview pane in <div data-theme="dark"> and everything inside re-themes with zero JavaScript.

Keep spacing and type on a scale

Colour gets all the attention, but spacing tokens are where inconsistency actually shows. Pick a scale and forbid off-scale values in review.

tokens/scale.css
1:root {2  --space-1: 0.25rem;  --space-2: 0.5rem;   --space-3: 0.75rem;3  --space-4: 1rem;      --space-6: 1.5rem;   --space-8: 2rem;4  --space-12: 3rem;     --space-16: 4rem;    --space-24: 6rem;5 6  --text-sm: 0.875rem/1.5;7  --text-base: 1rem/1.6;8  --text-lg: 1.125rem/1.6;9  --text-2xl: 1.5rem/1.2;10}

Wire it into Tailwind v4

In Tailwind v4 the theme *is* CSS variables, so your semantic tokens become utilities directly through @theme.

app/globals.css
1@import "tailwindcss";2 3@theme inline {4  --color-background: var(--color-background);5  --color-foreground: var(--color-foreground);6  --color-primary: var(--color-primary);7  --color-border: var(--color-border);8  --radius-lg: var(--radius);9}10 11/* now `bg-primary`, `text-foreground`, `border-border`,12   `rounded-lg` all resolve to the semantic tokens */

The audit that catches drift

A quick grep in CI for hardcoded colour values and raw primitive references in component files. It does not need to be clever. It just needs to fail the build when someone reaches past the semantic layer.

scripts/token-lint.sh
1#!/usr/bin/env bash2set -e3 4# no raw hex / rgb / oklch literals in component files5if grep -rnE '#[0-9a-fA-F]{3,8}|rgb\(|oklch\(' src/components; then6  echo "✗ hardcoded colour in a component — use a semantic token"7  exit 18fi9 10# no primitive tokens referenced outside the token layer11if grep -rnE 'var\(--(zinc|green|blue|red)-[0-9]' src/components src/app; then12  echo "✗ primitive token used directly — reference a semantic token"13  exit 114fi15 16echo "✓ tokens clean"
A token is a promise that a value has a single source of truth. Break that promise once — one hardcoded hex in one component — and the next person stops trusting the system.

Found this useful? Pass it on.

All posts

Want a custom write-up for your team? Get in touch.

Building something like this?

If a post here maps to a problem on your roadmap, that's usually a good sign we should talk.