Skip to main content
CSS

OKLCH and modern CSS colors: why your palette looks off in some browsers

A palette that looks perfect on your P3 laptop can wash out on an sRGB monitor or vanish in an old webview. The gamut and fallback mistakes behind it.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 3 min read

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

ModelLightnessGamut reachReach for it when
Hex / rgb()none (device values)sRGBFallbacks, legacy support
hsl()perceptually unevensRGBQuick tweaks, wide support
lch()perceptualwide (Lab-based)Similar goals, less hue-uniform than OKLCH
oklch()perceptual, hue-stablewide (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.

References

Primary documentation and specifications checked when this article was last updated.

CSSColorDesign

Related articles

All articles