The loudest token
a system ships.
Color carries brand, hierarchy, state, and meaning all at once — and it's the first token a stakeholder will argue about. A color system is the structure that lets you change every value without changing a single component.
01 — Why a system
Color is a name, not a value.
A modern color system has roughly fifteen hex values doing the work of fifty. The trick is naming — every component asks for --foreground, not for #0F172A. The name is stable; the value behind it can change per theme, per brand, per rebrand, without touching a single button.
That indirection is the entire game. Three things follow from it: dark mode becomes a token swap instead of a redesign, accessibility audits cover the whole product at once, and a rebrand stops being a six-month project.
02 — Token layers
Three layers. One direction.
Tokens stack. Primitives at the bottom are the raw palette. Semantic aliases in the middle bind primitives to a job. Component tokens at the top exist only when a semantic isn't expressive enough. References point downward — never up, never sideways.
Top tokens reference middle tokens. Middle tokens reference primitives. Direction is one-way.
01
Primitive
The raw palette — every shade you'll ever ship. Named only by hue and step. Never referenced directly from a component.
Examples
blue-600neutral-100red-500Internal — system reference only
02
Semantic
Purpose-bound aliases pointing at primitives. The layer components actually consume. Swappable per theme without rewriting components.
Examples
foregroundmuteddestructivebrandEvery component in the library
03
Component
Only when a semantic token isn't expressive enough. Reserved for high-traffic components where the token is part of the API.
Examples
button-primary-bgcard-borderinput-focus-ringRare — usually 5–10 tokens total
03 — The scale
Eleven steps, perceptually even.
Each hue gets an 11-step lightness ramp from 50 (page wash) to 950 (dark surface). Modern systems generate ramps in OKLCH so steps look evenly spaced to the eye — HSL produces visible jumps in the middle and crushes detail at the ends.
97%
93%
86%
76%
64%
52%
44%
36%
28%
18%
12%
Five hues × eleven steps = 55 primitives. Brand, neutral, success, warning, destructive. Anything else is a semantic alias.
04 — Semantic palette
Six roles do most of the work.
Components don't ask for blue. They ask for --brand. Every role pairs a surface with its readable ink. Pairs travel together — never split.
--brand
Primary actions, links, focused emphasis
--foreground
Body text, headings, default ink
--muted
Subtle surfaces, secondary chrome
--success
Completion, validation, positive state
--warning
Caution, unsaved changes, attention
--destructive
Delete, irreversible, errors
Heuristic
Every semantic role ships as a pair: --brand and --brand-foreground. If a designer uses one without the other, the audit catches it.
05 — Contrast
Three numbers decide who can read your product.
WCAG 2.1 gives three contrast thresholds. Body text needs 4.5:1. Large text (≥18pt or 14pt bold) can stop at 3:1. AAA at 7:1 is the recommended target for long-form reading. Anything below 3:1 is unreadable for most users with low vision — and brittle for everyone in glare.
WCAG 2.1 thresholds
Normal text targets AA (4.5:1). Large text (≥18pt or 14pt bold) can stop at 3:1. AAA is the recommended target for body text in long-form reading contexts.
Sample text · the brown fox.
Heading on background
Foreground · Background
Sample text · the brown fox.
Body on muted
Foreground · Muted
Sample text · the brown fox.
Caption on background
Muted-fg · Background
Sample text · the brown fox.
Action on brand
Brand-fg · Brand
Sample text · the brown fox.
Action on destructive
Dest-fg · Destructive
Sample text · the brown fox.
Placeholder ghost
Muted/60 · Background
The last card fails. Placeholder text at 40% opacity reads at 2.9:1 — visible to most, illegible to many. Either raise the opacity or move the hint outside the input.
06 — Dark mode
Not the inverse. A different shape of contrast.
Dark mode isn't light mode with the lightness flipped. Emission reads differently from reflection — pure white text on pure black vibrates. Lift the surfaces toward charcoal, soften the ink toward ivory, and reduce brand saturation. The token names stay; the values rebalance.
Light
Surface · default
Sample body text.
Dark
Surface · default
Sample body text.
Light mode uses pure white. Dark mode lifts off pure black (#000) by 10–15% to soften OLED bloom and reduce halation around small text.
Light
Surface · raised
Sample body text.
Dark
Surface · raised
Sample body text.
Cards and overlays go a shade darker in light mode (recessed) but lighter in dark mode (raised). Elevation reverses direction.
Light
Brand · primary
Sample body text.
Dark
Brand · primary
Sample body text.
Saturation gets pulled back in dark mode. A vibrant accent on a dark surface reads twice as loud as the same value on white — the eye dilates.
Heuristic
The semantic names stay identical across themes — only the primitive references swap. A button doesn't know which mode it's in.
07 — Anti-patterns
Four ways color systems fall apart.
Pulled from real design-system audits. Each one ships in week one, surfaces in month six, and takes a quarter to walk back.
Naming by appearance
Avoid
var(--blue-600)Prefer
var(--brand)Once the brand is no longer blue, every consumer needs a code change. Semantic tokens decouple meaning from value — primitives stay frozen, aliases evolve.
Inventing values inline
Avoid
background: #2e3447Prefer
background: var(--surface-2)One hand-typed hex becomes a hundred near-duplicates. Snap to the scale or extend it once — never both in the same week.
Color carrying all the meaning
Avoid
Red text means error · green means successPrefer
Icon + label + color · color is the third signal8% of men can't reliably distinguish red from green. Use color as reinforcement, not as the only signal — always pair with an icon or label.
Dark mode by inversion
Avoid
filter: invert(1) on the entire appPrefer
Paired dark tokens · different shape of contrastInversion breaks photos, charts, and brand color. Dark mode needs its own palette designed for emission rather than reflection — flatter shadows, softer brand, raised surfaces.
Always pair color with text or icon — never color alone.
Test in greyscale before you ship — does the hierarchy survive?
Audit at AA contrast minimum, target AAA for body text.
Continue
Color is one foundation. Typography reads it.
Type is where color lands. A good color system without a paired type system gives you a screenshot, not an interface. Read the type scale, the line-height rules, and the measure that makes long-form readable.