DEV Community

Irrational Apps
Irrational Apps

Posted on

Why your generated color palettes look muddy (and what OKLCH does about it)

I've built a handful of small color tools over the past year, and the thing that keeps surprising me is how bad HSL is at the one job people reach for it to do: generating a set of colors that look evenly spaced.

If you've ever built a palette by walking the hue wheel in HSL, keeping saturation and lightness fixed and stepping the hue, you've probably seen the result. The yellows and greens come out glaring, the blues and purples look dark and muddy, and the whole set feels lopsided even though the numbers insist every color sits at "50% lightness."

That isn't your monitor or your eyes. It's HSL.

Why HSL lightness lies to you

HSL's L channel is a geometric construct, not a perceptual one. It's derived from the max and min of the RGB channels, which has almost nothing to do with how bright a color actually looks to a person. Yellow at L=50% looks far brighter than blue at L=50%, because human vision is much more sensitive to the green-ish wavelengths that dominate yellow than to blue.

So when you generate N colors at fixed S and L with evenly spaced hue, perceived brightness swings wildly as you go around the wheel. For data visualization this is actively harmful. The bright categories jump forward and the dark ones recede, which implies an importance ranking that your data never had.

What OKLCH changes

OKLCH is a cylindrical form of the OKLab color space. It keeps the same three knobs you already know from HSL (a lightness, a chroma or "colorfulness", and a hue angle), but it's built on a model of human vision instead of raw RGB arithmetic.

L is perceptual lightness, so two colors at the same L genuinely look equally bright no matter their hue. C is chroma, roughly how vivid the color is. H is the hue angle in degrees.

The payoff is simple. Hold L and C steady, sweep H, and the palette reads as evenly weighted. Nothing leaps out, nothing disappears. And because browsers now support the oklch() function in CSS natively, you can take those values straight into a stylesheet without converting anything.

Here's the same idea in code:

// evenly spaced, perceptually uniform hues
function oklchPalette(count, l = 0.7, c = 0.13) {
  return Array.from({ length: count }, (_, i) => {
    const h = Math.round((360 / count) * i);
    return `oklch(${l} ${c} ${h})`;
  });
}
Enter fullscreen mode Exit fullscreen mode

Swap that same loop into HSL and the output looks uneven no matter how you tune it.

Two gotchas worth knowing

OKLCH describes a larger space than sRGB, so not every (L, C, H) triple maps to a real screen color. Push chroma too high at certain hues and you fall outside the gamut, the value gets clamped, and your carefully balanced palette quietly flattens in a couple of spots. Keeping chroma moderate, or letting your tool clamp per hue, avoids it.

Also worth saying plainly: "perceptually uniform" is not the same as "pretty." Uniform brightness is exactly what you want for categorical data viz where every series is equal. For a UI accent ramp you usually want the opposite, deliberate brightness steps from light to dark.

The tool I ended up building

I folded all of this into a generator: https://irrationaltools.com/color-palette-generator

It has three modes, because I kept needing different things on different days:

Distinct gives you N maximally distinguishable colors at uniform perceived brightness. This is the data-viz case, up to 30 colors, with 23 presets if you want a starting mood (Pastel, Ocean, Corporate, Medical, and so on).

Sequential builds multi-stop gradients with a choice of interpolation space and hue path (shortest, longest, clockwise, counter-clockwise around the wheel), plus a one-click CSS gradient export you can paste straight into a stylesheet.

From Image lets you upload a picture and pulls a palette out of it using k-means clustering, or you can click a single pixel to anchor a harmonic palette (complementary, triadic, analogous) off that color.

It runs entirely in the browser, nothing gets uploaded, there's no login, and you can copy any swatch as HEX, RGB, HSL, or OKLCH. It's free.

If you take one thing from this, let it be this: stop generating categorical palettes in HSL. Even if you never touch my tool, switch that loop to OKLCH and your charts will look a lot more honest.

Top comments (0)