Loading…
Loading…
amber-500 and indigo-500 sit on the same step of the palette. Measured, their lightness differs by 0.27. That is why a row of multi-coloured icons reads as noise.

Seven modules on this site each carry an identity colour. They sit next to each other in the navbar dropdown, in the mobile dock, in the command palette. I took the colours from Tailwind's -500 step, because that is what everybody does.
The result did not read as a system. The amber icon nearly vanished against a light background while the indigo one sat there heavy and dark. Same numeric step, same row, wildly different visual weight.
A step in Tailwind's palette is a naming convention, not a measurement. Convert them into OKLCH, where the first axis is perceived lightness, and the gap appears immediately:
amber-500 L 0.86
indigo-500 L 0.59
violet-500 L 0.54
That is 0.32 between the ends. It is not a defect in Tailwind: yellow is genuinely lighter than purple at the same saturation, and the ramp is built so each colour family steps pleasantly through its own range, not so different families match when placed side by side.
The problem only appears when you use the numeric step as though it were a shared plane. A grid mixing amber with indigo reads as noise, and it is the loudest "assembled from defaults" signal a site can emit.
These icons are meaningful indicators, so the contrast floor they need to clear is 3:1. Measured against the light background:
amber 2.02 : 1
emerald 2.32 : 1
Both fail. Not narrowly either, but at under two thirds of the threshold. For the small uppercase eyebrow text sharing those colours the bar is higher still, at 4.5:1.
The repair is to rebuild all seventeen colours on one plane: lock lightness and chroma per theme, and let only the hue angle move.
:root {
--cat-l: 0.52;
--cat-amber: oklch(var(--cat-l) 0.11 70); /* AA 5.33 */
--cat-emerald: oklch(var(--cat-l) 0.111 162); /* AA 4.91 */
--cat-indigo: oklch(var(--cat-l) 0.15 272); /* AA 5.39 */
}
.dark {
--cat-l: 0.74;
}
Two variables retune the whole set. Dark mode reads against a 0.12 page rather than a 0.98 one, so the plane moves up to 0.74. The worst member is now green at 4.88 on light and pink at 7.35 on dark. Every value clears AA, and no colour shouts over its neighbours.
This detail sounds backwards. If you lock lightness to make everything equal, why not lock chroma too?
Because that caps the set at its weakest member. sRGB has no dark saturated yellow, so holding every hue to yellow's ceiling at this lightness would drag the whole wheel down to C 0.085 and turn the vivid half dusty. Instead each hue is pushed to its own sRGB gamut boundary, multiplied by 0.97 for margin, then capped at 0.15.
That cap has its own reason: no category colour is allowed to be more saturated than the brand accent, the one colour on the site permitted to be loud.
You could write an out-of-gamut oklch() value and let the engine sort it out. I do not, because engines do not agree on how.
Some reduce chroma while holding lightness, as CSS Color 4 describes. Some clip per channel, and per-channel clipping shifts lightness. Which means you would do all that work to fix lightness, then hand the outcome back to a browser implementation detail. Writing the measured number down is how the original promise survives.
One more line from that file is worth copying. Every value carries a comment recording its contrast ratio against whichever background is worse. That is not decoration. If somebody adjusts --cat-l, all of those numbers become wrong at once, and the comments are the only thing telling them to measure again.
No comments yet — be the first!