-
Separate the three tiers before naming anything. A token system that conflates them is the
structural cause of drift.
- Primitive (a.k.a. global/option) tokens — raw, context-free values: the full colour
ramps, the type sizes, the space scale. Named by what they are, never where they're used:
color.blue.500, space.4, font.size.300. Components must NEVER reference these directly.
- Semantic (a.k.a. alias/system) tokens — role names that point at primitives and carry
intent:
color.text.default, color.surface.raised, color.action.primary.bg,
space.inset.md. This is the only tier a theme (dark, brand) re-points. It is the contract.
- Component tokens — the most specific tier, scoped to one component, pointing at semantic
tokens:
button.primary.bg, card.padding, input.border.focus. They exist so a single
component can be retuned without touching the shared semantic layer.
Each tier references only the tier above it: component → semantic → primitive → literal value.
Never let a component reach past semantic to a primitive — that is the single most common
failure and it silently breaks theming. See references/token-tiers-and-naming.md.
-
Set one naming convention and hold it. Use a consistent, hierarchical
namespace.category.concept.property.variant.state shape (drop segments that don't apply),
lower-kebab or dot-delimited, no abbreviations that aren't in a stated glossary. Decide
plural-vs-singular, and decide your scale unit names (100–900 numeric, or xs…3xl t-shirt)
once. A name must say its tier: a reader should tell a primitive from a semantic token by
the name alone. Banned names: anything that bakes a literal value into a role
(color.text.blue), positional cruft (color.box-2), or a primitive masquerading as a role.
-
Make semantic tokens the theming seam — never the primitives. Light/dark, each brand, and
each density mode are expressed by re-pointing the semantic layer at different primitives.
Components and their tokens stay byte-identical across themes; only the alias targets change.
This is what lets one component set serve N brands and both modes. For the dark remap, do NOT
invert lightness — dark surfaces want lower chroma and a near-dark grey carrying the brand hue,
accents usually lighten and slightly desaturate; re-run the contrast gate for the dark roles
independently (doctrine colour rule; 02-…/color-system-and-palette workflow). See
examples/semantic-mapping.md.
-
Encode colour primitives in OKLCH, not hex. Author the ramps in OKLCH (oklch(L C H)):
lightness is perceptual so ramp steps are actually even, hue stays put while you adjust
chroma, and generating a dark variant or a sibling brand is a controlled L/C move rather than
eyeballed hex-twiddling. Keep a hex fallback for legacy/email targets in the same token. This
is the perceptual-uniformity rule from the colour system, carried into the token layer.
-
Hold every themed semantic pair to the WCAG gate as a token-level invariant. A token set is
a promise that any sanctioned foreground/surface pairing passes: body/small text ≥ 4.5:1,
large text (≥24px or ≥18.66px bold) and meaningful UI/icons/focus ≥ 3:1
(doctrine/references/wcag-2.2-criteria.md, WCAG 1.4.3/1.4.11). Verify the pairs in each
theme — light passing does not imply dark passing. Record the results next to the tokens; a
token that can produce a failing pair is a bug, not a style choice.
-
Tokenise type, space, radius, elevation, and motion too — not just colour. Type sizes off
a real ratio (≥1.25; doctrine type-scale-and-spacing.md), space as multiples of one unit
(4 or 8px), a small radius set, an elevation set (shadow tokens that also remap for dark,
where elevation reads as lighter surface + lower-chroma shadow), and motion duration/easing
tokens. One rhythm, named once, is what separates "designed" from "assembled."
-
Choose the export topology, then generate. Single source of truth = the tier'd token file
(JSON, ideally aligned to the W3C Design Tokens $value/$type format). Transform out to:
CSS custom properties (semantic tokens become :root / [data-theme="dark"] / [data-brand]
vars), a JSON consumed by app code, a Tailwind theme, and native platforms — via Style
Dictionary so all targets derive from one source and cannot drift. See
references/token-export-formats.md and examples/tokens.json.
-
Document references, not values. The handoff is the semantic role map and the alias graph,
so engineering wires components to roles. A spec that lists hexes per component instead of role
names has already lost the plot. Hand the wired tokens to
09-…/component-library-architecture and 09-…/design-handoff-and-dev-spec.
-
Treat Apple materials as platform aliases, not primitives. Liquid Glass, vibrancy, system
backgrounds, SF Symbols rendering modes, and app icon variants should be named as semantic
platform roles with fallbacks. Do not hardcode a fake glass color or shadow token and call it
Liquid Glass; the platform material remains owned by the OS.