All entries
6 min read

OKLCH vs HSL vs hex — what actually matters

Three color formats, three jobs. Use the wrong one and your shade scales lie to you.

Generate a 50–950 shade scale

Three color formats are live on the web today. Hex (#ff5470), HSL (hsl(346 100% 66%)), and OKLCH (oklch(70% 0.22 0deg)). Each was designed for a different problem, and confusing them is why your shade scales look wrong.

Short version: hex is for storage, HSL is for rough math, OKLCH is for anything where you need the numbers to match what the human eye actually sees. If you’ve ever generated a shade ramp in HSL and watched the “darker” steps go muddy and lifeless, you’ve felt the difference.

Hex: an address, nothing more

#ff5470decodes to three bytes — R:255, G:84, B:112. That’s the entire content of hex. It’s a memory address for your color, not a description of it.

Hex is great for two things: passing colors between tools (a designer sends you a hex, you paste it into CSS, both of you see the same pixel), and writing down a color once you’ve already decided on it. It’s terrible for everything else.

You cannot look at two hexes and know which is brighter without pasting them somewhere. You cannot generate a shade ramp in hex because you’d need to mentally convert to another format first. The six digits hide every property a human cares about.

Don’t try to reason in hex. Store in it, then leave.

HSL: readable, but it lies

HSL was the first honest attempt at a color format designers could hold in their heads. Three numbers, each with a meaning: Hue (0–360°, position on the color wheel), Saturation (0–100%, chroma), Lightness (0–100%, brightness).

hsl(220 100% 50%) is a pure saturated blue at medium brightness. hsl(60 100% 50%)is a pure saturated yellow at the same brightness. Except — they’re nothing like the same brightness.

Put them next to each other. The yellow will hurt your eyes; the blue will look dim. Both claim L=50. One of them is lying.

This is because HSL’s “lightness” is a geometric midpoint between the hue at full intensity and its opposite, not a perceptual measurement. Your retina responds to yellow wavelengths harder than blue ones, so L=50% in yellow is visually much brighter than L=50% in blue.

The practical consequence: if you pick a brand hue and generate L=95, L=85, L=75... shade stops, the perceived darkness steps are uneven. Your 500 and 600 in blue collapse into each other; your 500 and 600 in yellow separate aggressively. Ramps look wrong in ways you can feel but not articulate, which is why everyone’s homegrown Tailwind configs look slightly off.

HSL is fine for rough eyeballing. It’s not fine for systems.

OKLCH: the one that matches your eyes

OKLCH is HSL’s perceptually-uniform replacement. Three numbers: Lightness (0–1 or 0–100%, in the Oklab space), Chroma (how colorful, no upper bound but practically 0–0.4), and Hue (0–360°).

The key phrase is perceptually uniform. A change from L=0.5 to L=0.6 in OKLCH produces the same perceived brightness jump regardless of hue. Blue at L=0.5 and yellow at L=0.5 actually look equally bright. Ramps generated with linear L steps step linearly to the eye.

Every evergreen browser has shipped OKLCH support since 2023. You can use it in CSS today:

.btn { background: oklch(0.7 0.22 340); }

Where it earns its keep:

  • Shade scales. A Tailwind-style 50–950 ramp generated in OKLCH stays visually even across the whole range. We use OKLCH by default on the /tailwind tool because it’s the only format that produces ramps that don’t look handmade.
  • Palette generation. Rotating hue in OKLCH produces colors that feel related even at different lightnesses. Rotate hue in HSL and half your outputs look washed, the other half like highlighters.
  • Contrast math that matches eyes. Since L is perceptual, L-differences in OKLCH correlate with WCAG contrast ratios much better than they do in HSL — which is why dark-mode blues look muddy until you fix them in a perceptual space.

The tradeoff: you can’t eyeball OKLCH as fluently. oklch(0.7 0.22 340)doesn’t read “pink” to your brain the way hsl(340 80% 60%)does. You’ll use a picker for a while.

When to use each

  • Hex — persistence, URLs, passing colors between systems. Everything stores as hex.
  • HSL— rough interactive picking when you want a dial with three obvious axes. Fine for “make this thing a bit darker.” Not fine for systems.
  • OKLCH — shade scales, palette generation, contrast calculations, anywhere accuracy matters.

The practical workflow: write and store hex. Generate scales and contrast-check in OKLCH under the hood. Let HSL stay in your head for the rare case when you’re sanity-checking a single color.

You don’t need to rewrite your CSS in OKLCH overnight. You do need to know which format your tools are using when they generate shade ramps — if it’s HSL, and your brand color isn’t yellow or cyan, your ramps are quietly wrong.

Try it
See any hex as a perceptually-uniform 50–950 scale

Drop a hex, get back a Tailwind-compatible shade scale generated in OKLCH by default. Flip to HSL to see why the perceptual version is worth it.

Read next

Keep going