Skip to content

Switching to OKLCH - On to a new gamut

Every color in this portfolio is now oklch(). Hex, rgb(), rgba(), hsl(), hsla(), all gone. 13 design tokens, a single dark mode mapping rule replacing 11 hand-tuned overrides, SVG chart labels, OG image gradients, even the animated avatar ring. And as a bonus: the whole site now reaches into the Display P3 color gamut with zero extra code.

Why

OKLCH fixes three problems that the old color spaces all share:

Perceptual uniformity. Equal L steps look equally bright, no matter the hue. oklch(0.5 ...) is visually halfway between black and white. HSL's lightness: 50% is all over the place. Yellow at hsl(60, 100%, 50%) is way brighter than blue at hsl(240, 100%, 50%).

Stable hue. In HSL, blue drifts toward purple as you change lightness (about 16° across a typical palette ramp). In OKLCH, hue stays put. You can generate an entire palette by varying L alone and the color family holds.

Independent chroma. Chroma in OKLCH is an absolute measure of colorfulness, separate from lightness. HSL saturation changes with lightness, so you can't keep vividness consistent across a scale. In OKLCH, equal chroma means equal vividness, independent of lightness, though the maximum achievable chroma varies by hue.

Put together, these three properties mean you can reason about color in code the way your eyes actually perceive it. A blue button and a yellow button with the same L value look equally bright, no surprises. A slate palette holds its hue across every step. And your vivid accent color stays vivid whether it's on a light background or a dark one.

What you actually get

Contrast fixes become a one channel problem. With hex, fixing contrast means trial and error across R, G, and B. In OKLCH you adjust L only. Chroma barely touches contrast.

Dark mode is a flip. Flip the L values in your palette mapping and the lightest step becomes the darkest. Perceptual uniformity means equal L steps work in both directions without hand tuning.

No hue drift in palettes. My secondary palette (slate) now has near-constant hue across all 11 steps, 257.2° to 257.4°. Well under the 10° drift threshold.

Beyond sRGB: P3 colors, no extra code

Hex, rgb(), and hsl() are all bound to the sRGB color space. They literally cannot express a color outside sRGB. Display P3 covers ~50% more colors, especially in cyans, reds, and purples.

OKLCH reaches P3 without any polyfill, media query, or fallback gymnastics. Push chroma past the sRGB ceiling and the browser renders P3 where supported, auto-clamping to the nearest sRGB color otherwise. Same oklch() value, progressive enhancement for free.

Your display:detecting…

Cyan (H=185°)

oklch(0.65 0.2 185)

P3-enhanced - auto-clamped on sRGB

oklch(0.65 0.09 185)

sRGB best-effort - max chroma at this hue and lightness

Red-Orange (H=25°)

oklch(0.55 0.28 25)

P3-enhanced - auto-clamped on sRGB

oklch(0.55 0.21 25)

sRGB best-effort - max chroma at this hue and lightness

Purple (H=310°)

oklch(0.5 0.35 310)

P3-enhanced - auto-clamped on sRGB

oklch(0.5 0.29 310)

sRGB best-effort - max chroma at this hue and lightness

Each pair shows the same P3-capable OKLCH value (top) alongside its sRGB-clamped equivalent (bottom). On a P3 display, the top swatches are visibly more vivid - a color you cannot express in hex, rgb(), or hsl(). On sRGB, both rows look identical because the browser automatically gamut-maps the top value down.

The demo uses three P3 colors: oklch() values whose chroma exceeds what sRGB can represent at that hue and lightness. No color(display-p3 ...) syntax. No @media (color-gamut: p3) wrapper. Just oklch().


The result is a color system that's simpler to reason about, consistent across light and dark modes, and quietly future-proof, rendering richer colors wherever the display allows, with no fallback code to maintain. If your design tokens are still hex, it's worth the switch.

References


Stay ahead of the curve in Web Development with Javascript Every Month Newsletter.

I will deliver a curated selection of articles, tutorials, and resources straight to your inbox once a month.

Read the Archives

Subscribe to JEM Newsletter

No spam, unsubscribe anytime.