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.
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.
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.
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.
Quick reference
| Rule | Value |
|---|---|
| Color space | OKLCH for perceptual uniformity |
| Token naming | Semantic (--primary) not literal (--blue-500) |
| Dark mode surface | Higher elevation = lighter background |
| Dark mode shadows | Reduce or remove — use surface lightness instead |
| Hover derivation | Light: L - 5%, Dark: L + 5% (OKLCH) |
| Flash prevention | Blocking 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