Primitives
Layer 1 — raw values that are theme-aware but never used directly in components.
Definition
Primitives are raw values. They are theme-aware (light/dark mode) but should never be used directly in components.
Naming Pattern
--{category}-{name}
Common categories include:
| Category | Example |
|---|---|
colors | --colors-neutral-500 |
size | --size-16 |
radii | --radii-08 |
font-family | --font-family-primary |
font-weight | --font-weight-500 |
font-size | --font-size-14 |
leading | --leading-20 |
Names typically use pixel-equivalent values (e.g. --font-size-14 = 0.875rem) for easy mental mapping.
Rules
- Never use primitives directly in component styles
- Primitives are only referenced when defining semantic tokens via
var() - Import primitives before semantics in CSS (semantics depend on primitive declarations)
- Never edit auto-generated files — use extension files for manual tokens
Color Space: OKLCH
Color primitives are defined in the OKLCH color space (oklch(L C H)).
| Channel | Meaning | Range |
|---|---|---|
| L (Lightness) | Perceptual brightness | 0 (black) – 1 (white) |
| C (Chroma) | Color intensity | 0 (grey) – ~0.37 (most vivid) |
| H (Hue) | Hue angle | 0° – 360° |
Why OKLCH over HSL or raw sRGB hex:
- Perceptual uniformity — equal numeric steps produce equal visual steps. Two colors at the same L value look equally bright, which makes contrast ratios predictable across an entire ramp.
- Wide gamut — OKLCH can represent Display P3 and Rec 2020 colors. Gamut mapping to sRGB is applied at build time so tokens degrade gracefully.
- Theme symmetry — inverting lightness for dark mode produces natural-looking results because L is perceptually linear.
Anchor Model
Each palette starts from one or more anchor colors — designer-chosen hue and chroma values pinned to specific steps in the ramp. The generator holds these anchors fixed and fills the remaining steps algorithmically.
An anchor defines:
- Hue (H) — the base hue angle for the palette
- Chroma (C) — peak color intensity at the anchor step
- Step — which ramp position (e.g.
500) the anchor occupies
Multiple anchors per palette are supported. When present, hue is interpolated between them using shortest-arc (circular) interpolation to prevent unexpected hue shifts.
Lightness Ramps
Each scheme (light, dark) has a fixed set of lightness targets — the L value assigned to each step. The two schemes are approximate mirrors of each other.
Light scheme (LIGHTNESS_TARGETS_LIGHT)
| Step | L |
|---|---|
| 50 | 0.98 |
| 100 | 0.95 |
| 200 | 0.92 |
| 300 | 0.88 |
| 400 | 0.82 |
| 500 | 0.75 |
| 600 | 0.65 |
| 700 | 0.52 |
| 800 | 0.45 |
| 900 | 0.35 |
| 950 | 0.25 |
| 1000 | 0.15 |
Dark scheme (LIGHTNESS_TARGETS_DARK)
| Step | L |
|---|---|
| 50 | 0.12 |
| 100 | 0.16 |
| 200 | 0.22 |
| 300 | 0.28 |
| 400 | 0.34 |
| 500 | 0.42 |
| 600 | 0.52 |
| 700 | 0.65 |
| 800 | 0.75 |
| 900 | 0.85 |
| 950 | 0.92 |
| 1000 | 0.98 |
These values are defined in
generate-ramps.tsand may evolve — treat the generator as the source of truth.
Generation Pipeline
generate-ramps.ts converts anchor definitions into primitive tokens through these steps:
- Parse anchors — load anchor hex values and their designated steps
- Convert to OKLCH — parse each anchor hex into the OKLCH color space
- Set lightness — assign L from the appropriate lightness-targets array (light or dark scheme)
- Interpolate hue — for steps between anchors, interpolate H via shortest-arc to avoid crossing through unrelated hues
- Distribute chroma — apply a bell-curve distribution (σ = 0.28) centred on the peak-chroma anchor, with a floor at 8% of peak chroma to prevent fully desaturated mid-ramp steps
- Gamut-map — clamp chroma via
clampChroma()so every output color falls within sRGB - Emit tokens — output as CSS custom properties, JSON, and Figma variable schemas