OKLCH Explained: The Perceptual Color Space That Beats HSL for Ramps
OKLCH is the CSS Color 4 space that makes tints, shades and gradients look evenly spaced. Learn what L, C and H mean, why it beats HSL, and how to convert.
OKLCH Explained: The Perceptual Color Space That Beats HSL for Ramps
The first time I built a button palette in HSL, I picked a base blue, kept the saturation and hue fixed, and stepped the lightness in even 10% jumps. On paper it was a clean ramp. On screen the light end bunched together and the dark end fell off a cliff. I spent an afternoon hand-tuning numbers that should have been mechanical. The problem was not me. It was the color space.
OKLCH fixes exactly this. It is a perceptually uniform color space, and once you understand what that phrase actually buys you, a lot of color work that used to be fiddly becomes arithmetic.
What OKLCH actually is
OKLCH is a color model standardized in the CSS Color 4 specification. It is the cylindrical (polar) form of OKLab, a color space published by Björn Ottosson in 2020 that was fitted to how human vision actually perceives lightness and hue. Where OKLab uses three rectangular axes, OKLCH rewrites them into the three numbers a designer thinks in: lightness, chroma, and hue.
You write it as oklch(L C H). For example, oklch(62% 0.17 145) is a medium-bright, fairly saturated green. That string drops straight into any CSS color property — color: oklch(62% 0.17 145) — in every current browser.
The key word is perceptual. In OKLCH, equal numeric steps are designed to look like equal visual steps. Go from oklch(40% 0.1 264) to oklch(50% 0.1 264) and your eye reads one even brightness jump. That guarantee is the whole reason the space exists, and it is precisely the guarantee HSL cannot make.
What L, C and H mean
Three values, and each one earns its place:
- L — Lightness. Runs from 0 (black) to 100% (the brightest the space allows). Unlike HSL's lightness, this tracks perceived brightness, so a yellow and a blue at the same L genuinely look equally bright.
- C — Chroma. Roughly how colorful the value is. It starts at 0 (a pure gray) and rises with no fixed ceiling, though inside sRGB it tops out around 0.37. A typical vivid web color sits near 0.15–0.25.
- H — Hue. An angle from 0 to 360 degrees, the same wheel idea as HSL — 145 is green, 264 is blue — but spaced according to perception rather than raw math.
A useful mental model: set C to 0 and you get a gray at whatever lightness L holds, no matter the hue. Raise C and you push that gray toward a vivid color along the H direction. One of the most common beginner mistakes is reading chroma as a 0–100 percentage; it is not. Type 50 expecting half-saturation and you will land far outside any real display's range.
Why OKLCH beats HSL for ramps and gradients
HSL spaces its hue and lightness by formula, not by how eyes work. The consequence is that its lightness is uneven across hues, and its chroma is exaggerated near the edges of the gamut. That is why the HSL tint-and-shade ramp I described at the top fell apart: holding saturation and hue fixed while stepping lightness drifts the hue and bunches the light values, because the underlying math never promised otherwise.
OKLCH sits on the OKLab model, which was fit to human vision. So when you hold C and H fixed and step L, you get a clean, evenly spaced ramp of tints and shades. The same discipline that failed in HSL just works.
Gradients tell the same story. A blue-to-yellow gradient interpolated in sRGB or HSL slumps through a dull gray-green middle, because lightness and chroma sag along the path. Interpolate the same two endpoints in OKLCH and the perceived lightness stays even, so the midpoint stays vivid instead of going muddy. If you have ever stared at a gradient wondering why the center looks dead, this is almost always the cause.
A real conversion, start to finish
Take a common brand blue, #1d4ed8. Run it through the OKLCH Color Converter and you get back, among other formats:
oklch(50.05% 0.218 264.05)
Now build a ramp from it. Hold chroma and hue fixed at 0.218 and 264, and step lightness in even amounts:
oklch(30% 0.218 264) /* deep, for pressed states */
oklch(45% 0.218 264)
oklch(60% 0.218 264)
oklch(75% 0.218 264)
oklch(90% 0.218 264) /* pale, for tinted backgrounds */
Each line is the same brightness step as the last, so the five swatches read as a coherent family. Try the equivalent in HSL and the top two will crowd together while the hue quietly wanders. Converting once and stepping L is the entire technique.
There is one honest caveat the converter surfaces for you. OKLCH can describe colors more vivid than an sRGB monitor can show. Push chroma too far — say oklch(70% 0.37 145) — and a channel falls outside the sRGB cube. The tool flags that with an out-of-gamut warning and shows the clamped nearest renderable color, so a wide-gamut value you grabbed from a design tool never silently shifts on you. If you need the HEX and the OKLCH to agree on screen, lower C until the warning clears.
Browser support and a practical workflow
OKLCH ships in every current evergreen browser, so you can write it directly in production CSS today. For the long tail of older clients, the usual pattern is a fallback rule:
.btn {
background: #1d4ed8; /* legacy fallback */
background: oklch(50% 0.218 264); /* modern, perceptual */
}
A browser that understands OKLCH takes the second declaration; one that does not keeps the HEX. That is also why a converter stays useful even after full support arrives: designers pick in OKLCH, but token files, older tooling, and shared specs still speak HEX, RGB, and HSL. When you need to check that a ramp keeps enough contrast at each step, pair the converter with a color contrast checker so your accessible-text decisions ride on the same lightness values you tuned.
My own habit now is simple: design the system in OKLCH, generate every state by nudging L, and convert to HEX only at the export boundary. The colors come out coherent on the first try, and the afternoon I once lost to hand-tuning an HSL ramp does not repeat.
Made by Toolora · Updated 2026-06-13