Color System
Colors are the most complex part of the design system. This page explains how they’re structured, from raw LCH values to the semantic pairs developers use in code.
design-tokens/ ├─ core/ │ ├─ colors_lch.json # 15 color families × 14 stops — LCH source of truth │ ├─ colors_hex.json # HEX fallback (Figma export only) │ └─ ... # dimensions.json, typography.json │ ├─ mode/ # Semantic color tokens — one file per theme variant │ ├─ light/ # Each file maps: bg, text, border, state, set.* │ │ ├─ default.json # bg.0→{gray.25} text.1→{pure.black} set.brand.primary.bg→{indigo.700} │ │ ├─ dimmer.json # Reduced contrast: bg.0→{gray.50} │ │ ├─ high-contrast.json # Max contrast: bg.0→{pure.white} │ │ ├─ default-deuteranopia.json │ │ ├─ default-protanopia.json │ │ ├─ default-tritanopia.json │ │ └─ ... # 12 variants total (3 contrast × 4 vision) │ └─ dark/ # Same structure, inverted mappings │ ├─ default.json # bg.0→{pure.black} text.1→{gray.25} set.brand.primary.bg→{blue.500} │ ├─ dimmer.json # bg.0 darken(0.80) text.1→{gray.150} │ └─ ... # 12 variants total │ └─ components/desktop/base/ # Component dimensions only (spacing, radius, sizing) └─ ... # 26 files — no colors here # All color tokens live in mode/ files, including: # bg.*, text.*, border.*, state.*, set.*, comp.*, gradient.*, shadow.*
Token Studio JSON (design-tokens/ — single source of truth) │ ├──→ Figma Variables # synced via Token Studio plugin │ └──→ Style Dictionary # scripts/build.js │ ▼ CSS Custom Properties --cn-gray-500: lch(65% 6 272) --cn-text-2: var(--cn-gray-850) --cn-set-brand-primary-bg: var(--cn-indigo-700) │ ▼ Tailwind Utilities # tailwind-design-system.ts text-cn-2, bg-cn-brand-primary, border-cn-3, ...
Mode Contrast Vision Adaptation ───── ──────── ───────────────── light × default × standard dark dimmer deuteranopia (red-green) high-contrast protanopia (red-green) tritanopia (blue-yellow) Example: dark/dimmer-deuteranopia.json ├─ inherits dark/dimmer.json (contrast adjustments via darken modifiers) └─ overrides danger colors (red → orange shift for visibility)
Why LCH?
All colors are authored in LCH color space (Lightness, Chroma, Hue). LCH is perceptually uniform — equal numeric steps produce equal visual differences. This makes it straightforward to build consistent color ramps and verify contrast ratios mathematically.
HEX values exist only as a Figma export fallback. LCH is the source of truth.
{ "gray": { "25": { "$value": "lch(99% 0 272)" }, "50": { "$value": "lch(97% 0 272)" }, "100": { "$value": "lch(92% 1 272)" }, "500": { "$value": "lch(65% 6 272)" }, "900": { "$value": "lch(16% 6 280)" }, "1000": { "$value": "lch(7.5% 1 280)" } }}Each color family has 14 stops (25–1000) plus pure.white and pure.black. The full palette includes: gray, red, blue, indigo, forest, yellow, purple, cyan, mint, orange, pink, violet, brown, lime, amber.
Semantic Levels
Core primitives have no meaning — gray.500 is just a value. Semantic tokens assign purpose. They’re defined per-theme in design-tokens/mode/{dark,light}/ and map intent to value.
Here’s how the same semantic tokens resolve to different core primitives depending on the active theme:
| Semantic token | Light | Dark | Role |
|---|---|---|---|
bg.0 | gray.25 | pure.black | Navigation, sidebar |
bg.1 | pure.white | gray.1000 | App background |
text.1 | pure.black | gray.25 | Headings, max contrast |
text.2 | gray.850 | gray.200 | Body text |
text.3 | gray.700 | gray.500 | Secondary, metadata |
border.1 | gray.500 | gray.500 | Strong borders, focus |
border.2 | gray.150 | gray.850 | Default borders |
The pattern is consistent: light themes pick from the light end of a ramp (25–500), dark themes pick from the dark end (500–1000). This is why components never reference core primitives directly — the theme file handles the mapping.
Backgrounds
Four levels create depth hierarchy — from navigation chrome to floating surfaces:
<div className="bg-cn-0">Navigation, sidebar</div><div className="bg-cn-1">Application background</div><div className="bg-cn-2">Cards, containers, form fields</div><div className="bg-cn-3">Popovers, tooltips</div>Text
Four neutral levels create typographic hierarchy:
<h2 className="text-cn-1">Headings, labels — max contrast</h2><p className="text-cn-2">Body text, default content</p><span className="text-cn-3">Secondary text, metadata</span><span className="text-cn-4">Disabled text</span>Semantic text colors for status and brand — these are shortcuts that reference color sets internally, so the theme file picks the right shade automatically:
text-cn-success
text-cn-danger
text-cn-warning
text-cn-brand
text-cn-merged
<span className="text-cn-success">Passed</span><span className="text-cn-danger">Failed</span><span className="text-cn-warning">Pending</span><span className="text-cn-brand">Link</span><span className="text-cn-merged">Merged</span>Borders
Three neutral levels from strong to subtle:
<div className="border border-cn-1">Focus rings, hover outlines</div><div className="border border-cn-2">Cards, form fields, dividers</div><div className="border border-cn-3">Table rows, subtle separators</div>Semantic border colors for status and brand:
<div className="border border-cn-brand">Focus rings, active inputs</div><div className="border border-cn-success">Success alert borders</div><div className="border border-cn-danger">Error alert borders</div><div className="border border-cn-warning">Warning alert borders</div>Interactive States
Overlay colors for hover and selection feedback:
<div className="bg-cn-hover">Hover overlay</div><div className="bg-cn-selected">Selected overlay</div>Color Sets — The Core Pattern
The most important architectural concept. Color sets are pre-built color pairs — coordinated background + text combinations for different usage scenarios. Each pair is verified for WCAG contrast compliance, so developers never need to manually check accessibility.
Every semantic color is defined as a set with three emphasis levels:
set.{color}.primary → bg, bg-hover, bg-selected, text, ringset.{color}.secondary → bg, bg-hover, bg-selected, textset.{color}.outline → bg, bg-hover, bg-selected, text, border| Level | Background | Text | Use case |
|---|---|---|---|
| primary | Saturated fill | High-contrast (usually black/white) | Buttons, strong badges |
| secondary | Muted tint | Colored | Soft badges, subtle highlights |
| outline | Near-transparent | Colored | Alerts, bordered containers |
Here’s how set.success resolves to core primitives across themes — notice that light uses lime while dark switches to forest for better contrast on dark surfaces:
| Token | Light | Dark |
|---|---|---|
set.success.primary.bg | lime.700 | forest.400 |
set.success.primary.text | pure.white | pure.black |
set.success.secondary.bg | lime.100 | forest.900 |
set.success.secondary.text | lime.800 | forest.150 |
set.success.outline.border | lime.150 | forest.800 |
The same structure repeats for all 14 color sets (brand, danger, warning, blue, purple, etc.). Secondary and outline levels flip between light and dark ends of the ramp — and some sets switch color families entirely between themes.
Each set has three levels visualized below — primary (saturated fill), secondary (muted tint), outline (near-transparent with border):
{ /* ✅ Correct — same set, same level */}<Badge className="bg-cn-success-secondary text-cn-success-secondary"> Done</Badge>;
{ /* ❌ Wrong — mixed levels break contrast */}<Badge className="bg-cn-success-secondary text-cn-success-primary">Done</Badge>;Component-Specific Colors
Some components need colors that don’t fit the generic semantic tokens — gradients, overlays with custom alpha, or domain-specific highlights. These live in the comp.* namespace and reference semantic tokens internally:
{ "comp.alert.fade.danger": "linear-gradient(0deg, {set.danger.secondary.bg} 42%, ...)", "comp.dialog.backdrop": "{gray.600} + alpha(0.2)", "comp.diff.add-content": "{set.success.secondary.bg} + alpha(0.45)", "comp.avatar.shadow": "{gray.1000} + alpha(0.08)"}Component tokens follow the same theme-switching rules — their source values (set.*, bg.*, gray.*) are resolved per theme, so component colors adapt automatically.
State Tokens & Color Modifiers
Interactive states use the $extensions system from Token Studio to apply LCH-space transformations:
{ "state": { "hover": { "$value": "{gray.850}", "$extensions": { "studio.tokens": { "modify": { "type": "alpha", "value": "0.2", "space": "lch" } } } } }}This produces lch(from var(--cn-gray-850) l c h / 0.2) — a relative-color alpha overlay. The same modifier system supports darken and lighten operations for contrast variant themes (dimmer, high-contrast).
Available Modifiers
Token Studio supports four color modifiers:
- alpha — adjusts opacity (0 = transparent, 1 = solid)
- lighten — increases lightness by percentage (0.2 = 20% lighter)
- darken — reduces lightness by percentage (0.2 = 20% darker)
- mix — blends with another color (0.5 = 50/50 mix)