Engineering
Why I abandoned HEX and HSL for OKLCH in Tailwind v4
Engineering color for perceptual uniformity, accessibility, and the P3 gamut.
The problem with HSL
Its lightness number does not mean lightness.
HEX and RGB describe what a monitor should emit, not what a person sees. HSL looks like the fix, because it exposes a lightness value you can reason about. But that value is a lie: HSL lightness is computed from RGB channels, not from human perception.
The contrast roulette
This site began as a project for a school, where a parent with low vision is not an edge case. Guessing at contrast was not acceptable, so the design system needed a color model whose numbers tell the truth.
Enter OKLCH
A color space built on measured perception.
OKLCH, from CSS Color Level 4, describes color as lightness, chroma, and hue in the Oklab space, which is fitted to how people actually perceive brightness. Its key property is perceptual uniformity: two colors with the same L value look equally bright, whatever their hue.
L: lightness
0 to 1. The same L value is the same perceived brightness for every hue, which makes contrast something you can calculate.
C: chroma
Color intensity from gray outward. Lowering it desaturates cleanly instead of drifting toward mud.
H: hue
The angle on the color wheel. Rotating it keeps lightness and intensity stable, so hue shifts stay smooth.
The comparison below shows five hues pinned to the same lightness in each model. The HSL row jumps between bright and dark; the OKLCH row holds steady.
HSL — 50% lightness
Non-uniformSame mathematical lightness, wildly different perceived brightness
OKLCH — 0.6 lightness
Perceptually uniformSame perceptual lightness, identical perceived brightness
HSL blue at 50% lightness appears 3.4× darker than HSL yellow at 50% lightness. In OKLCH, all five colors share identical perceived brightness.
Beyond sRGB
HEX cannot name the colors modern screens show.
There is a second argument. HEX and HSL are locked to the sRGB gamut, while most current phones and laptops render the wider Display P3 gamut. OKLCH can address those colors directly, and the browser falls back to sRGB on hardware that cannot show them.
35%
Legacy sRGB
Of the visible spectrum
+25%
Display P3
More colors than sRGB
95%+
Browser support
For oklch() in CSS
Implementation in Tailwind v4
Primitive ramps, semantic aliases, utilities.
Tailwind v4 is CSS-first: tokens are plain custom properties, and the @theme block turns them into utility classes with opacity modifiers included. This site defines a small set of OKLCH primitives, aliases them to semantic names, and exposes only the semantic layer to components. This is the actual token source of the page you are reading.
/* theme.css: the single source of design values */
:root {
/* Primitive ramps, all OKLCH */
--paper-100: oklch(0.965 0.013 84);
--ink-900: oklch(0.245 0.02 60);
--green-700: oklch(0.4 0.075 152);
--green-800: oklch(0.34 0.07 152);
/* Semantic aliases */
--background: var(--paper-100);
--foreground: var(--ink-900);
--primary: var(--green-700);
--primary-strong: var(--green-800);
}
@theme inline {
/* Tailwind v4 turns these into utilities,
opacity modifiers included */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
}The discipline pays off in maintenance. Deriving a hover state means lowering L a step while keeping C and H, and the result stays on palette. Checking contrast means comparing two L values instead of eyeballing screenshots.
See the tokens
The complete OKLCH theme and the Tailwind v4 setup are in the public repository.