Colour systems

HSL's lightness axis lies. OKLCH fixes it.

HSL lies. Yellow at `hsl(60, 100%, 50%)` and blue at `hsl(240, 100%, 50%)` share the same L value, but yellow looks twice as bright. Add a second hue and you're hand-tuning the entire scale again. ## OKLCH: perceptually uniform colour - **L** (Lightness): 0 = black, 1 = white. - **C** (Chroma): Vividness. 0 = grey. Maximum depends on hue. - **H** (Hue): 0-360 degrees. ## Building a palette with CSS variables ```css :root { --brand-50: oklch(0.98 0.01 250); --brand-100: oklch(0.93 0.02 250); --brand-200: oklch(0.87 0.04 250); --brand-300: oklch(0.78 0.06 250); --brand-400: oklch(0.68 0.1 250); --brand-500: oklch(0.58 0.14 250); --brand-600: oklch(0.48 0.14 250); --brand-700: oklch(0.38 0.12 250); --brand-800: oklch(0.28 0.08 250); --brand-900: oklch(0.18 0.04 250); } ``` Chroma tapers at extremes because light and dark shades clip at high chroma. To add a second hue, change only the hue angle and chroma curve. Lightness stays the same. ## Avoiding pure black and pure white `#000` on `#FFF` is 21:1 contrast, but harsh. Pure black on pure white creates a vibrating edge. ```css :root { --text: oklch(0.15 0.01 250); /* near-black, not black */ --background: oklch(0.99 0.005 250); /* near-white, not white */ } ``` ~18:1 contrast. Far above WCAG AA, far more comfortable. ## Dark mode mapping Dark mode is not a reversed light palette: ```css [data-theme="dark"] { --background: oklch(0.15 0.01 250); --text: oklch(0.92 0.01 250); --surface: oklch(0.2 0.015 250); --border: oklch(0.28 0.02 250); --muted: oklch(0.55 0.03 250); } ``` - **Low background chroma.** Saturated dark backgrounds create colour noise. - **High text lightness, not 1.0.** Pure white glares. - **Surface sits close to background.** Small lightness jumps. - **Borders need more contrast.** `rgba(0,0,0,0.1)` vanishes. Use `rgba(255,255,255,0.1)`. ## Common mistakes - **HSL palettes with uneven mid-tones.** Switch to OKLCH. - **Pure `#000` on pure `#FFF`.** Soften both ends. - **Light grey body text.** `#999` on `#fff` is 2.85:1. Fails WCAG AA. - **Reversing the palette for dark mode.** Map dark mode independently. - **Brand colours as links without contrast checks.** Many teals and oranges fail against white.