The hard part of a color system is not generating 11 attractive swatches. It is deciding which roles exist, which pairs must remain legible, how wide-gamut intent degrades, and how future contributors can understand the decision.
The core problem
Raw colors answer “what value is this?” Semantic tokens answer “what job does it do?” If components point directly to purple-600, a brand change becomes a search-and-replace problem. If they point to action-primary, the palette can evolve behind a stable contract.
A six-stage workflow
- Inventory roles. List surfaces, text, borders, focus, actions, success, warning and danger before choosing values.
- Create raw ramps. Use OKLCH lightness as the main hierarchy and shape chroma away from gamut-constrained extremes.
- Choose a delivery gamut. Ship sRGB as the baseline. Add P3 only where the product and QA process can support it.
- Assign semantic aliases. Map raw steps to product jobs instead of letting components select arbitrary swatches.
- Verify pairs in context. Check actual foreground/background pairs, focus indicators and non-text states.
- Export and review. Store CSS, Tailwind or DTCG tokens in source control with rationale and visual regression coverage.
Separate raw and semantic layers
:root {
--violet-700: oklch(47% 0.18 305);
--paper-50: oklch(98% 0.01 85);
--action-primary: var(--violet-700);
--action-primary-label: var(--paper-50);
}
Three common use cases
| Use case | Start with | Verify |
|---|---|---|
| Brand ramp | One approved middle brand color | Hue drift, gamut mapping, logo constraints and interaction states |
| Data visualization | Harmony with a controlled L/C slice | Adjacent distinguishability, labels, patterns and color-vision scenarios |
| Dark theme | Semantic roles, not an inverted light ramp | Surface elevation, text flare, focus and disabled states |
Contrast is a pair property
WCAG 2.2’s contrast thresholds apply to rendered foreground and background colors. A palette step cannot be “accessible” by itself. The generator lets you change the surface and reports every swatch against that same mapped color. Recheck after opacity and overlays are applied.
Practical FAQ
Should every ramp have 11 stops?
No. Eleven aligns neatly with Tailwind’s 50–950 convention, but a small product may need five deliberate roles. More tokens create more unsupported choices.
Can one ramp serve light and dark themes?
The raw ramp can be shared, but semantic mappings should be theme-specific. Dark surfaces often need different chroma and contrast decisions rather than a simple reversed index.
Should P3 replace sRGB?
Use sRGB as the dependable fallback. P3 can be a progressive enhancement where extra chroma has product value and is tested on compatible hardware.
Where should the approved values live?
In a versioned source of truth close to the code that consumes them. DTCG JSON is useful for tool interchange; CSS or Tailwind variables are useful at runtime.
Generate the raw layer.
Then name semantic roles in your own system.
Sources and further reading
Technical claims are grounded in specifications and primary documentation. Product observations are dated snapshots from direct use.