OKLCH Color Space & Dark Mode

Perceptually uniform colors that actually work

8 min read10 rulesBeginner

Why HSL lies about brightness

HSL claims blue and yellow at the same lightness are equally bright. Your eyes disagree. OKLCH fixes this — equal lightness values actually look equally bright across all hues. Drag the hue slider and watch the difference.

HSL vs OKLCH — Perceptual Uniformity
Loading...

Name colors by purpose, not appearance

--color-blue-500 breaks in dark mode. --color-primary works everywhere. Semantic naming decouples intent from implementation — you change the palette once, every component adapts.

Hardcoded vs Semantic Tokens
Loading...

Surface hierarchy in dark mode

In light mode, elevation = shadow. In dark mode, shadows are invisible — elevation is expressed through surface lightness. Higher elements get a lighter background. Toggle between themes to see how the same card stack adapts.

Light vs Dark — Surface Elevation
Loading...

Deriving hover states from base colors

Don't hardcode hover colors. Derive them by adjusting lightness: light mode → darker on hover, dark mode → lighter on hover. OKLCH makes this trivial because lightness adjustments are perceptually linear.

Hover Derivation — Lightness Shift
Loading...

Quick reference

RuleValue
Color spaceOKLCH for perceptual uniformity
Token namingSemantic (--primary) not literal (--blue-500)
Dark mode surfaceHigher elevation = lighter background
Dark mode shadowsReduce or remove — use surface lightness instead
Hover derivationLight: L - 5%, Dark: L + 5% (OKLCH)
Flash preventionBlocking script in <head> to set theme before paint

Key takeaways

  • Use OKLCH instead of HSL — equal lightness values actually look equal
  • Name tokens by purpose (--primary) not appearance (--blue-500)
  • Dark mode elevation = lighter surface, not shadows
  • Derive hover colors by shifting OKLCH lightness, don't hardcode
  • Block theme flash with a synchronous script in the document head
oklchdark-modesemantic-tokenssurfacecolor