Skip to content

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.

Color Token Structure
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.*
Build Pipeline
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, ...
Theme Composition (2 × 3 × 4 = 24 variants)
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 tokenLightDarkRole
bg.0gray.25pure.blackNavigation, sidebar
bg.1pure.whitegray.1000App background
text.1pure.blackgray.25Headings, max contrast
text.2gray.850gray.200Body text
text.3gray.700gray.500Secondary, metadata
border.1gray.500gray.500Strong borders, focus
border.2gray.150gray.850Default 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:

bg-cn-0
bg-cn-1
bg-cn-2
bg-cn-3
<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:

text-cn-1
text-cn-2
text-cn-3
text-cn-4
<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:

border-cn-1
border-cn-2
border-cn-3
<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:

border-cn-brand
border-cn-success
border-cn-danger
border-cn-warning
<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:

bg-cn-hover
bg-cn-selected
<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, ring
set.{color}.secondary → bg, bg-hover, bg-selected, text
set.{color}.outline → bg, bg-hover, bg-selected, text, border
LevelBackgroundTextUse case
primarySaturated fill

High-contrast (usually black/white)

Buttons, strong badges
secondaryMuted tintColoredSoft badges, subtle highlights
outlineNear-transparentColoredAlerts, 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:

TokenLightDark
set.success.primary.bglime.700forest.400
set.success.primary.textpure.whitepure.black
set.success.secondary.bglime.100forest.900
set.success.secondary.textlime.800forest.150
set.success.outline.borderlime.150forest.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)

Further Reading