Inset

Color in OKLCH

Why the numbers in your color picker have been lying to you, and how a perceptually uniform space fixes palettes without taste getting involved.

NKNabin Khair
2 min read
Color in OKLCH

Pick a blue and a yellow in HSL, both at lightness: 50%. They are not the same brightness. The yellow reads as a highlight, the blue reads as a shadow. Your palette drifts because the number you trusted was never measuring the thing you assumed it measured.

This is a model problem, not a taste problem. Fix the model and most color decisions stop being arguments.

What perceptually uniform means

OKLCH is built so that equal steps in lightness are equal steps in brightness to the eye. oklch(0.5 ...) is visually mid-gray regardless of hue. Hue holds steady as lightness changes — a blue at 0.2 and the same blue at 0.8 are the same blue, not a blue that has slid toward purple.

--brand: oklch(0.637 0.237 25.331);
--brand-muted: oklch(0.637 0.12 25.331);

Same lightness, same hue, less chroma. The muted version reads as the brand turned down, not as a different color. That predictability is the whole point.

A few thresholds that hold

  • A light background wants foreground lightness below 0.35 for readable text.
  • A dark background wants foreground lightness above 0.9.
  • When contrast fails, move the L channel. Chroma barely moves the needle on legibility, and shifting it also shifts the hue you chose.

Dark mode is a reversal

The cleanest dark themes are not a second design. They are the light theme with the lightness mapping reversed — keep the hue and chroma, invert the L. The result feels like the same product at night, not a different one you have to relearn.

:root {
  --background: oklch(1 0 0);
  --foreground: oklch(0.145 0 0);
}
.dark {
  --background: oklch(0.145 0 0);
  --foreground: oklch(0.985 0 0);
}

What to leave alone

Not every color has to be OKLCH today. Existing hex values that work can stay until you have a reason to touch them. The rule is for new code: reach for oklch() first, and let the model do the work your eye used to do badly.

In HSL you guess and adjust until it looks right. In OKLCH the numbers mean what you think they mean, so you usually get it right the first time.

Comments(5)

YO

Supports Markdown formatting.

  • MC
    Maya Chen

    The concentric radius tip alone was worth the read. I’ve been matching outer and inner radii for years without realizing why it felt off.

    outer = inner + padding — that’s the whole rule.

    • SO
      Sam Ortiz

      Same — once you see it, you can’t unsee the mismatch on nested cards.

      • Cards
      • Dialogs
      • Comment threads (ironically)
      • MC
        Maya Chen

        Outer = inner + padding is the whole rule.

        Exactly. Glad this landed.

    • RN
      Riley Ng

      Also useful with Typeset measure caps — nesting and measure fight each other if you’re not careful.

  • JB
    Jordan Blake

    Clear writing on a topic that usually gets hand-waved. Would love a follow-up on how this plays with nested dialogs.

Related posts