Skip to main content
Blog
Design SystemsMay 12, 2026·6 min read

OKLCH: The color space your design system has been waiting for

Why we abandoned hex and HSL for perceptual color, and how OKLCH gave us a dark mode that actually works across every screen.

The problem with hex and HSL

Hex codes are machine notation. #3b82f6 tells you nothing useful — you have to load it into a color picker to understand what you're looking at. Experienced designers can sometimes squint at a hex value and guess the hue, but the lightness and saturation are completely opaque. That alone makes hex a poor authoring format for design tokens, but the deeper problem shows up when you try to build a palette.

HSL was supposed to fix this. Hue, saturation, lightness — intuitive, right? In practice, HSL's L channel is perceptually non-uniform. Set hsl(120 100% 50%) and you get a blinding lime green that feels far brighter thanhsl(240 100% 50%), a deep blue — even though both claim 50% lightness. Building an accessible color palette in HSL means hand-tuning every shade individually to compensate for this unevenness. Your 500-step greens need different numeric lightness values than your 500-step blues just to look visually equivalent. It is tedious, error-prone, and breaks down the moment you add a new hue to the system.

Dark mode makes this worse. Inverting HSL palettes naively produces colors that look washed out or fluorescent. Designers end up maintaining two entirely separate sets of hand-tuned tokens — one for light, one for dark — and keeping them in sync becomes a full-time job.

What OKLCH actually is

OKLCH is part of CSS Color Level 4, shipping in all major browsers since late 2023. It defines colors with three channels: L (perceptual lightness, 0–1), C (chroma, roughly saturation, 0–0.4), and H (hue angle, 0–360). The key word is perceptual. An L value of 0.5 truly looks middle-gray regardless of the hue. You can build a 10-step scale across any number of hues just by sweeping L from 0.1 to 0.95, and every step at the same L will feel equally bright.

The “OK” in OKLCH stands for the Björn Ottosson color model on which it is based. It improves on its predecessor LCH by eliminating hue shift artifacts (where blues would drift purple under certain transformations). OKLCH is now the recommended starting point for any color science work in CSS.

A concrete example: oklch(0.7 0.15 30) describes a warm orange at exactly 70% perceived brightness. Change the hue to oklch(0.7 0.15 260) and you get a muted indigo at the same perceived brightness — no manual compensation needed. This predictability is the entire point.

/* Warm orange, 70% perceived brightness */
oklch(0.7 0.15 30)

/* Same L, same C — now indigo */
oklch(0.7 0.15 260)

/* You can also use percentage syntax */
oklch(70% 0.15 30)

Building a real scale with ParticleUI tokens

ParticleUI's dark theme starts with an almost-black background: oklch(0.04 0.005 60). That reads as: 4% perceptual brightness, nearly zero chroma, hue 60 (yellow-warm). The chroma nudge toward yellow gives the background the “warm espresso” character rather than cold neutral gray. The cream foreground sits at oklch(0.96 0.01 80) — 96% bright, a whisper of golden chroma. Both values were chosen by typing numbers into oklch.com and watching the swatch, not by converting from some hex value found in a Figma file.

The CSS custom properties pattern that results is clean and intentional:

/* globals.css — dark theme (default) */
:root {
  --color-bg:        oklch(0.04 0.005 60);   /* warm near-black */
  --color-surface-1: oklch(0.08 0.005 60);   /* card backgrounds */
  --color-surface-2: oklch(0.12 0.006 60);   /* hover surfaces */
  --color-border:    oklch(0.18 0.008 60);   /* subtle dividers */

  --color-text-1:    oklch(0.96 0.010 80);   /* primary text */
  --color-text-2:    oklch(0.78 0.008 80);   /* body text */
  --color-text-3:    oklch(0.55 0.006 80);   /* muted text */
  --color-text-4:    oklch(0.38 0.004 80);   /* disabled / hints */

  --color-accent:    oklch(0.78 0.17  58);   /* golden amber */
}

Notice how readable this is. Every value tells you exactly where it sits in the brightness stack. You can add a new surface level — say oklch(0.10 0.005 60) — without guessing whether it will feel right. It will, because L is linear with perception.

Dark mode that actually works

This is where OKLCH earns its keep. Because L is perceptual, a light theme is just an inverted L scale — the chroma and hue stay the same, only lightness flips. Compare the two approaches:

/* HSL dark mode — every value hand-tuned separately */
:root[data-theme="dark"]  { --bg: hsl(220  14%   8%); }
:root[data-theme="light"] { --bg: hsl(220  14%  98%); } /* fine */

:root[data-theme="dark"]  { --text: hsl(220  10%  90%); }
:root[data-theme="light"] { --text: hsl(220  10%  12%); } /* also fine... */

/* ...but blues look different from greens at the "same" saturation.
   You end up tuning hsl(140 60% 45%) vs hsl(220 60% 62%) by hand
   just to achieve equal perceived weight. */

/* OKLCH light mode — flip L, keep C and H */
:root {
  --color-bg:     oklch(0.98 0.005 60);  /* was 0.04, now 0.98 */
  --color-text-1: oklch(0.08 0.010 80);  /* was 0.96, now 0.08 */
  --color-accent: oklch(0.52 0.17  58);  /* same hue, same chroma, darker L */
}

The accent color in light mode uses the same chroma (0.17) and hue (58) as dark mode — only L changes from 0.78 to 0.52 to maintain contrast against a light background. No new color was invented. No Figma hand-off saying “use #B45309 in light mode.” The math is the design.

Accessibility note: OKLCH's perceptual uniformity makes contrast ratio calculation more predictable. If your text L is 0.08 and your background L is 0.98, the contrast is excellent. You can encode this as a rule rather than checking every combination individually.

Try it

The best way to start is oklch.com — a live color picker that outputs CSS-ready oklch() values. Pick your brand's primary hue on the H slider, set chroma to taste, then sweep L from 0.1 to 0.9 to generate a full scale. If you want to see how gradients behave, the CSS gradient playground at gradient.style lets you compare interpolation methods — OKLCH gradients look dramatically better than RGB through midpoints.

You do not need to migrate your entire design system at once. Pick one token — your primary brand color — and define it in OKLCH. Build a 5-step scale around it. See how it feels in dark mode. The perceptual uniformity becomes obvious immediately, and from there the rest of the migration is mechanical. ParticleUI's entire token set started with a single amber swatch. It grew from there, one logical step at a time.

OKLCH: The color space your design system has been waiting for — ParticleUI Blog | ParticleUI