From Brand Color to Design Tokens: Building a Usable Scale
How to turn one or two brand colors into a token system that survives dark mode, accessibility checks and real product surfaces — naming, scale generation, and the semantic layer.
A brand guideline usually hands you two or three HEX values. A product needs somewhere between 40 and 100 distinct color decisions. The gap between those two facts is what a token system closes.
Three layers, in order
Skipping straight from brand color to component styles is what produces the "we have 14 shades of blue and nobody knows which to use" situation. Build three layers instead:
1. Primitives. Raw values with mechanical names: blue-50 through blue-900, gray-50
through gray-900. No opinion about usage. These are the only place literal HEX values appear.
2. Semantic tokens. Purpose-named aliases pointing at primitives: --color-surface,
--color-text-primary, --color-text-muted, --color-border, --color-primary,
--color-danger. Components reference only this layer.
3. Component tokens. Optional, and only when a component genuinely deviates:
--button-primary-bg. Most systems need very few of these.
The payoff is that dark mode becomes a remapping of layer 2. Layer 1 stays fixed, layer 3 rarely changes, and no component file is touched.
Generating the primitive scale
From one brand color, produce 9 to 11 steps. The mechanics:
- Convert the brand color to HSL or OKLCH so hue is isolated
- Hold hue constant and distribute lightness across the range, roughly 97% down to 12%
- Adjust saturation in a curve — higher at the light and dark ends, slightly lower in the middle, or mid-tones look muddy
- Place the original brand color at whichever step it naturally lands on, usually 500 or 600
Two details that matter more than they look:
The steps should not be linear in lightness. Perceived difference compresses at the extremes, so even lightness spacing produces steps that feel uneven near white and near black. Tighter spacing at the ends reads better.
Check which steps pass as text early. For most hues, only steps 600 and darker clear 4.5:1 on white, and only 300 and lighter clear it on near-black. Knowing this before naming tokens prevents a semantic token that can never be accessible.
Neutrals are not gray
Pure gray next to a saturated brand palette looks lifeless. Build the neutral scale from the brand hue at 4–8% saturation. The tint is imperceptible in isolation and makes the interface feel intentional.
Keep the neutral scale longer than the color scales — most interface surface decisions land in neutrals, and you will want fine gradations for borders and dividers.
Naming that survives
Rules that prevent churn:
- Never name a token after its value.
--color-blue-buttonbreaks the day the button turns green.--color-primarydoes not. - Never name a token after one usage site.
--color-sidebar-bginvites duplication when the same surface appears elsewhere.--color-surface-raiseddescribes the role. - Encode the pair, not just the color. For every surface token, define the foreground token
meant to sit on it.
surfacepluson-surfaceprevents the accessibility failures that come from arbitrary combinations. - Keep the state suffix consistent.
-hover,-active,-disabledon every interactive token, even when a value repeats.
Semantic tokens you almost certainly need
A minimal but complete set:
- Surfaces:
background,surface,surface-raised,surface-overlay - Text:
text-primary,text-secondary,text-muted,text-inverse - Lines:
border,border-strong,divider - Intent:
primary,primary-hover,danger,warning,success,info— each with a foreground counterpart - Focus:
focus-ring, which must hit 3:1 against every adjacent surface
Anything beyond this should be added because a real screen demanded it.
Ship it as CSS variables
Define primitives once, then remap semantics per theme:
:root { --color-surface: var(--blue-50); } and
.dark { --color-surface: var(--blue-950); }
Components read var(--color-surface) and never know which theme is active. This is the same
pattern shadcn/ui uses, which is why swapping its theme is a matter of editing a handful of
variables rather than touching components.
Try it
- Palette Generator — generate the hue scales your primitives come from
- Color Converter — move brand HEX values into HSL for scale math and back
- Contrast Checker — validate every surface-plus-foreground token pair
Related reading: Designing a Dark Mode Color System covers the layer-2 remapping in detail, and Color Spaces for Web Work explains why OKLCH produces more even scales than HSL.