Design tokens
A token system is what stops every page being a fresh set of arbitrary decisions. It is also what makes theming, printing, and accessibility tractable — you fix a value once instead of hunting hex codes.
Keep it small. A page needs perhaps 30 tokens, not 300.
Define by role, never by appearance
The most important rule. Name what a token is for, not what it looks like.
/* WRONG - the name lies the moment you add a dark theme */
--light-grey: #e5e7eb;
--blue-500: #3b82f6;
/* RIGHT - the role holds in every theme */
--surface: #ffffff;
--surface-raised: #f7f8fa;
--border: #e3e6ea;
--ink: #14161a; /* primary text */
--ink-muted: #5b6472; /* secondary text */
--ink-subtle: #98a2b3; /* tertiary, ticks */
--accent: #2f5fd8;
--accent-ink: #ffffff; /* text ON accent */
--ink-muted still means "secondary text" in dark mode. --light-grey becomes a lie.
The full set
:root {
/* surfaces + text */
--surface: #fff; --surface-raised: #f7f8fa; --surface-sunken: #eef1f5;
--border: #e3e6ea; --border-strong: #c8cdd6;
--ink: #14161a; --ink-muted: #5b6472; --ink-subtle: #98a2b3;
/* accent + semantics */
--accent: #2f5fd8; --accent-ink: #fff; --accent-wash: #eaf0fd;
--positive: #1f7a4d; --negative: #b3261e; --warning: #9a6700;
--positive-wash: #e8f4ee; --negative-wash: #fbeceb;
/* spacing */
--space-1: 4px; --space-2: 8px; --space-3: 12px; --space-4: 16px;
--space-5: 24px; --space-6: 32px; --space-7: 48px; --space-8: 64px;
/* type */
--text-xs: 11px; --text-sm: 13px; --text-base: 15px;
--text-lg: 18px; --text-xl: 24px; --text-2xl: 36px;
--font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
/* shape */
--radius-sm: 3px; --radius-md: 6px; --radius-lg: 10px;
--elev-1: 0 1px 2px rgb(0 0 0 / .06);
--elev-2: 0 4px 12px rgb(0 0 0 / .10);
}
Three radii, two elevations. If you find yourself wanting a fourth radius, you probably want one of the existing three.
Semantic direction is not red/green
Finance UIs default to red-down / green-green. Two problems: it fails for the most common colour vision deficiency, and up is not always good — a rise in churn, DSO, or burn is bad.
So:
- Encode direction with a glyph (▲ ▼) or the sign, always. Colour is reinforcement.
- Choose colour by whether it is good, not by direction — and let the caller decide which.
--positive/--negativeare about goodness; the arrow is about direction. - Prefer a blue/orange pair over red/green where the semantics allow it; it survives every common form of colour blindness.
<span class="delta bad">▲ 4.2 pts</span> <!-- churn rose: up, and bad -->
Chart tokens
Charts need their own tokens, and they should not be the UI accent colours reused at random.
:root {
/* categorical - ordered by how often they get used */
--series-1: #2f5fd8; --series-2: #d9770a; --series-3: #1f7a4d;
--series-4: #7a3fbf; --series-5: #b3261e; --series-6: #0e7490;
/* sequential ramp - for heatmaps, 5 steps is usually enough */
--seq-1: #eef3fb; --seq-2: #cfdcf6; --seq-3: #9db8ec;
--seq-4: #5c86dd; --seq-5: #2f5fd8;
/* diverging - only where a meaningful midpoint exists (variance vs plan) */
--div-neg: #b3261e; --div-mid: #f3f4f6; --div-pos: #1f7a4d;
/* chart furniture */
--grid: #eceff3; --axis: #c8cdd6; --tick: var(--ink-subtle);
--bar-neutral: #98a2b3; /* anchors in a bridge */
}
Rules that matter more than the exact hues:
- Categorical for unordered categories only. Using a categorical palette for an ordered thing (aging buckets, cohort age) discards the ordering the reader needs.
- Sequential for magnitude, diverging only with a real midpoint. Variance versus plan has one: zero. Revenue does not.
- Cap categorical at ~6. Beyond that, group into "other" or switch to small multiples — nobody matches an eighth colour to a legend.
- The same segment gets the same colour on every chart in the page. Assign a colour per entity once, globally, not per chart.
Verify every pair against artifact-accessibility for contrast and greyscale separation. A palette
that only works in colour is half a palette.
Theming
Define the complete light palette on bare :root, then redefine only the tokens that change:
:root { /* full light palette - every token defined here */ }
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) { --surface: #101215; --ink: #e8eaed; /* … */ }
}
:root[data-theme="dark"] { --surface: #101215; --ink: #e8eaed; /* … */ }
Never give a colour its only definition inside a media query or a [data-theme] block. That is
the bug that produces an unstyled page in the third theme state. See artifact-theming for why
there are three states and how to verify all of them.
Using them
Reference tokens; never hardcode a value in a component.
.kpi { padding: var(--space-4); background: var(--surface-raised);
border-radius: var(--radius-md); }
.kpi__label { font-size: var(--text-sm); color: var(--ink-muted); }
.kpi__value { font-size: var(--text-2xl); color: var(--ink);
font-variant-numeric: tabular-nums; line-height: 1.1; }
A hardcoded hex or px inside a component is a token you have not named yet. Either add it to the scale or use the nearest existing step — almost always the latter.
Reuse across a set of pages
When several artifacts belong together, keep the token block byte-identical across them. Paste the
same :root into each page (there is no shared stylesheet under a strict CSP — see
artifact-architecture). Consistency here is what makes a set of pages read as one product rather
than as several unrelated tools.
Related skills
artifact-theming— the three-state theme mechanics these tokens servevisual-hierarchy,data-typography— the scales in useartifact-accessibility— contrast and greyscale validationui-antipatterns— what defaulting these produces