COLOR SYSTEM

Gabriele – Frontend developer

What is a color system?

A color system defines roles, scales (shade sets), and usage rules — to make UI consistent, accessible, and easily themable.

Core roles

  • Brand / Accent – CTAs, links, active states.
  • Neutrals – backgrounds, cards, text, borders.
  • Semanticok, warn, info, danger.
  • Stateshover, active, focus, disabled.

Use OKLCH for predictable contrast

oklch(L C H) lets you control lightness (L) and contrast more clearly than HSL. Lock the brand hue via H, then adjust only L to create shades.

:root{
  /* Brand hue (change once for theming) */
  --brand-h: 260;
  /* Accent scale */
  --accent-50 : oklch(0.97 0.02 var(--brand-h));
  --accent-100: oklch(0.92 0.04 var(--brand-h));
  --accent-300: oklch(0.78 0.10 var(--brand-h));
  --accent-500: oklch(0.64 0.14 var(--brand-h)); /* primary */
  --accent-600: oklch(0.58 0.14 var(--brand-h));
  --accent-700: oklch(0.52 0.12 var(--brand-h));

  /* Neutrals (ink & surfaces) – only L changes */
  --ink-900 : oklch(0.22 0 0);
  --ink-700 : oklch(0.35 0 0);
  --ink-500 : oklch(0.55 0 0);
  --surface  : oklch(0.98 0 0);
  --card     : oklch(0.99 0 0);
  --border   : oklch(0.90 0 0);

  /* Semantic */
  --ok     : oklch(0.62 0.12 150);
  --warn   : oklch(0.78 0.13 80);
  --danger : oklch(0.62 0.15 25);
}

Role-based tokens

UI never uses “raw” colors – only role tokens (makes theming easy).

:root{
  --bg: var(--surface);
  --text: var(--ink-900);
  --muted: var(--ink-700);
  --link: var(--accent-500);
  --link-hover: var(--accent-600);
  --focus-ring: color-mix(in oklab, var(--accent-500) 35%, transparent);
}
a{ color: var(--link); }
a:hover{ color: var(--link-hover); }
:focus-visible{ outline: 3px solid var(--focus-ring); outline-offset: 2px; }

States: hover / active / disabled

Create derivatives with color-mix() — no need to keep dozens of variables.

.btn{
  --btn-bg: var(--accent-500);
  --btn-fg: white;
  background: var(--btn-bg);
  color: var(--btn-fg);
  border: 1px solid color-mix(in oklab, var(--btn-bg) 70%, black);
}
.btn:hover{
  background: color-mix(in oklab, var(--btn-bg) 88%, black);
}
.btn:active{
  background: color-mix(in oklab, var(--btn-bg) 78%, black);
}
.btn[disabled]{
  background: color-mix(in oklab, var(--btn-bg) 25%, var(--surface));
  color: color-mix(in oklab, var(--btn-fg) 60%, var(--muted));
  opacity: .7; pointer-events: none;
}

Elevation: surfaces, cards, overlays

As layers rise, increase contrast and add soft shadows.

.surface-1{ background: var(--surface); }
.card{
  background: var(--card);
  border: 1px solid var(--border);
  box-shadow: 0 1px 2px rgba(0,0,0,.05), 0 10px 24px rgba(0,0,0,.06);
}
.overlay{
  background: color-mix(in oklab, black 40%, transparent);
  backdrop-filter: blur(2px);
}

Dark mode (single toggle)

For the same H, raise L for text and lower it for backgrounds. Invert neutrals.

:root[data-theme="slate"]{
  --surface: oklch(0.14 0 0);
  --card:    oklch(0.18 0 0);
  --border:  oklch(0.28 0 0);
  --text:    oklch(0.92 0 0);
  --muted:   oklch(0.78 0 0);

  /* Accent – slightly lighter for contrast on dark */
  --accent-500: oklch(0.72 0.14 var(--brand-h));
  --link: var(--accent-500);
  --link-hover: oklch(0.80 0.12 var(--brand-h));
}

Contrast rules (quick)

  • Body text ≥ 4.5:1, large headings ≥ 3:1.
  • On colored backgrounds, use “ink on brand” pairs: on-accent, on-danger, etc.
:root{
  --on-accent: white; /* if L < .6 → use white, otherwise ink-900 */
}
.badge{ background: var(--accent-100); color: var(--ink-900); }
.cta{ background: var(--accent-500); color: var(--on-accent); }

Theming: change only the base

For each theme, change --brand-h and, if needed, neutral L levels.

:root[data-theme="ocean"]{  --brand-h: 230; } /* blue */
:root[data-theme="forest"]{ --brand-h: 150; } /* green */
:root[data-theme="rose"]{   --brand-h: 350; } /* pink */

Quick checklist

6–8 step fluid scale with clamp() Base text 16–18px, body LH 1.45–1.6 Measure: 60–75ch Contrast ≥ WCAG AA Preload fonts + font-display: swap + metrics override Semantic tokens: body, lead, kicker, caption, code