You build a vivid brand palette in OKLCH on a MacBook, it looks saturated and gorgeous, you ship it. Then a teammate on an external sRGB monitor says the accent color looks muddy, and a user on an in-app browser reports the primary buttons have no background at all. Nothing is broken in your CSS. You've hit the two things OKLCH quietly demands: gamut awareness and a fallback.
OKLCH exists because HSL lies about lightness. hsl(60 100% 50%) (yellow) and hsl(240 100% 50%) (blue) claim the same 50% lightness, but the yellow is blindingly brighter to your eye. OKLCH's L is perceptual, so equal L values actually look equally bright across hues — which is what makes generating a consistent ramp possible instead of eyeballing every step.
.button {
background: #2563eb; /* fallback, seen by old engines */
background: oklch(0.55 0.18 255); /* modern engines override */
}
The mistakes behind "it looks off"
Shipping OKLCH with no fallback
Support landed in Safari 15.4 back in March 2022, then Chrome 111 and Firefox 113 in 2023 rounded it out to Baseline. That covers most traffic, but not embedded webviews in older apps, or a user pinned to Safari 15.0. When an engine doesn't understand oklch(), the whole declaration is invalid and thrown away — the element gets no background, not a wrong one. Always declare a hex or rgb() first, then the oklch() line, so the cascade falls back cleanly. Or gate it with @supports (color: oklch(0 0 0)).
Pushing chroma past the sRGB gamut
C has no hard maximum, and it's easy to write oklch(0.7 0.3 150) — a green more saturated than sRGB can represent. Browsers clamp out-of-gamut colors back into the available gamut, but how they clamp varies, and a P3 display shows more of that chroma than an sRGB one. That's the exact reason your color looked richer on the MacBook than on the external monitor. Keep chroma within sRGB (roughly ≤0.13 at mid lightness for most hues) unless you deliberately want the P3 boost and have tested the sRGB fallback.
Assuming equal L means equal contrast
OKLCH lightness is perceptual, but it is not WCAG relative luminance. Two swatches at L: 0.5 are not guaranteed to pass a 4.5:1 contrast ratio against the same background. Perceptual uniformity helps you build even-looking ramps; it doesn't excuse you from running an actual contrast check on the rendered text.
Letting gradients interpolate hue the long way
linear-gradient(in oklch, red, blue) interpolates through hue space, and by default hue takes the shorter arc — which can pass through a purple you didn't intend, or the wrong purple. When a gradient looks like it detours through a strange color, set the hue interpolation explicitly (in oklch shorter hue / longer hue) rather than trusting the default.
OKLCH vs the models you're replacing
| Model | Lightness | Gamut reach | Reach for it when |
|---|---|---|---|
Hex / rgb() | none (device values) | sRGB | Fallbacks, legacy support |
hsl() | perceptually uneven | sRGB | Quick tweaks, wide support |
lch() | perceptual | wide (Lab-based) | Similar goals, less hue-uniform than OKLCH |
oklch() | perceptual, hue-stable | wide (up to P3+) | Design tokens and consistent ramps |
Adopt OKLCH for the tokens where a predictable, even ramp actually pays off — then validate the two things it can't guarantee for you: that older engines get a real color, and that your chroma still looks right on an sRGB panel you don't own.
Cover photo by Stanislav Kondratiev on Pexels.
