OKLCH color picker and converter

Move lightness, chroma and hue independently and read the result back in every CSS format. The chroma track marks the point where the color leaves sRGB, so you can see how much headroom you actually have before anything gets clamped.

0% is black, 100% is white, and the steps between are perceptually even.

Colorfulness, with no fixed upper bound. The markers show where this lightness and hue leave sRGB and Display P3.

Degrees around the hue circle. Holding hue while changing the other two keeps the color recognisable.

Every format

    Gamut

    • sRGBEvery screen, every browser
    • Display P3Recent Apple hardware, most OLED phones
    • Rec. 2020Broadcast HDR; no consumer display covers it fully

    Reading the three axes

    OKLCH describes a color the way a person would: how light it is, how colorful it is, and what hue it is. That sounds like HSL, and the syntax looks similar, but the numbers behave very differently — which is the whole reason to use it.

    Lightness runs from 0% to 100% and is perceptual. Move it from 40% to 50% and the color looks exactly as much lighter as moving from 70% to 80%. HSL does not give you this: hsl(60 100% 50%) and hsl(240 100% 50%) both claim 50% lightness, while the yellow is obviously bright and the blue is obviously dark. That single difference is why a ramp built in OKLCH steps evenly and the same ramp in HSL has a washed-out middle and a muddy end.

    Chroma is colorfulness, and unlike HSL saturation it is not a percentage of anything. It starts at 0 for grey and has no defined ceiling, because the ceiling belongs to the display rather than the color. Real values top out somewhere around 0.37, but the useful maximum at any given moment depends on the other two axes — which is what the markers on the chroma track are for.

    Hue is an angle in degrees. Roughly: 30 is red, 70 orange, 110 yellow-green, 145 green, 195 cyan, 250 blue, 300 purple, 350 pink. The hues are spaced more evenly than in HSL, where a large stretch of the circle is taken up by greens and blues get squeezed.

    Why chroma runs out at different points

    The set of colors a screen can show is a cube in RGB, but that cube becomes a lopsided, lumpy solid when you re-plot it in OKLCH. Its widest point sits around 60–70% lightness, and it is much wider at some hues than others — yellows and greens hold far more chroma than blues and purples do.

    So there is no single number to cap the chroma slider at. Set lightness to 95% and almost no chroma is available at any hue; set it to 65% and a saturated blue has plenty. This is the one genuinely awkward thing about OKLCH compared with HSL, where saturation is always 0–100% and always means something. The trade is worth making, but it does mean a picker has to show you the boundary rather than pretend it is not there.

    Going past the boundary is not an error. A color outside sRGB is a real color that some displays can show, and the browser will map it to something displayable on the ones that cannot. What matters is that the mapping is your decision.

    Writing a fallback

    Two separate things can go wrong, and they need different fallbacks. A browser too old to understand oklch() will discard the declaration entirely, so it needs a previous declaration to fall back to. A browser that understands it perfectly but sits in front of an sRGB monitor will gamut-map instead, which needs nothing from you.

    The first case is handled by declaring the fallback first and the OKLCH value second, since later declarations win and unparseable ones are dropped:

    .button {
      background: #4f7cf0;                    /* every browser */
      background: oklch(60% 0.18 262);        /* dropped by old ones */
    }

    That is enough for most work. Where you want a genuinely different value on wide-gamut screens rather than a mapped approximation of one, wrap the upgrade in a feature query:

    .button { background: #4f7cf0; }
    
    @supports (color: oklch(0% 0 0)) {
      .button { background: oklch(60% 0.22 262); }
    }

    Copy the HEX row above for the first line and the OKLCH row for the second — the tool already reduces chroma for the HEX value, so the fallback is the closest sRGB match rather than a clipped one.

    Where this is worth the effort

    Three jobs where OKLCH pays for itself immediately. Color scales: hold chroma and hue, walk lightness, and you get a ramp whose steps look even — try the color picker to see one generated from any starting color.Gradients: interpolating in OKLCH keeps the midpoint saturated instead of passing through grey, which is what the gradient maker does. Theming: because lightness is perceptual, you can derive a whole dark-mode palette by flipping lightness values and trust that the contrast relationships roughly hold — then confirm them in the contrast checker.

    Where it is not worth it: a one-off brand color that a designer handed you as a hex code. Converting it to OKLCH gains nothing on its own. The value appears when you need a second color that relates to the first.

    Common questions

    What does OKLCH stand for?
    Ok — from the Oklab color space it is built on — plus Lightness, Chroma and Hue. It is the cylindrical form of Oklab, the same relationship HSL has to a cube of RGB.
    Why does chroma have no maximum?
    Because the maximum depends on the other two values. OKLCH describes colors independently of any screen, and how much chroma a screen can actually show changes with lightness and hue. A chroma of 0.2 is ordinary for a mid blue and impossible for a pale yellow. In practice values run from 0 to about 0.37.
    Is OKLCH better than HSL?
    For anything involving a series of colors, yes. HSL lightness is not perceptual: hsl(60 100% 50%) and hsl(240 100% 50%) claim the same lightness while yellow looks far brighter than blue. OKLCH lightness matches what the eye sees, so ramps step evenly and gradients do not go grey in the middle.
    Can I use oklch() in production?
    Yes. It is supported in current Chrome, Edge, Safari and Firefox. If you need to support older versions, declare a hex or rgb() value first and override it inside @supports (color: oklch(0% 0 0)), so old browsers take the fallback and new ones the wide-gamut value.
    What happens if a color is outside sRGB?
    The browser gamut-maps it to something displayable, and on an sRGB monitor you see the approximation rather than the value you wrote. The color is still valid — it simply needs a fallback so the mapping is your choice rather than the browser’s.