HSL, lightness 50%
ColorTokensKit, stop _350
Every HSL swatch says 50% lightness, yet the lightness your eyes see (L*) runs from 32 to 97. The ColorTokensKit row holds one L* for every hue. Switch to “Lightness only” to see it.
These are twelve hues at HSL lightness 50%, and the same hues at our _350. Switch to "Lightness only" to take the hue away. The HSL row turns into grays from dark to nearly white, because HSL ignores how much brighter green looks than blue. Ours turns into one gray.
That's why we built the palette in OKLCH: every hue can share one lightness at each stop, so _600 is 4.68:1 on white in every hue, and you can swap families without checking contrast again. How did we choose and build these colors? has the short version.
How OKLCH describes a color#
OKLCH gives a color a lightness (L), a chroma (C, how far it is from gray) and a hue (H, an angle around the wheel). Björn Ottosson designed it in 2020 so that equal steps in the numbers look like equal steps in color. CSS writes the same numbers as oklch().
Why hue and chroma come from OKLCH#
Color.proBlue._50.toLCH().h // 258°
Color.proBlue._600.toLCH().h // 272°, like every darker stopEvery proBlue stop sits at OKLCH hue 250°, and they read as one blue from light to dark. Measured in CIELab LCH, the same stops turn 14°. So a blue built at one LCH hue drifts toward purple as it gets lighter, while OKLCH holds it steady, and we take hue and chroma from OKLCH.
Why lightness comes from CIELab L*#
let coral = OKLCHColor(l: 0.57, c: 0.08, h: 40) // #A06753, L* 49.2, 4.61:1 on white
let teal = OKLCHColor(l: 0.57, c: 0.08, h: 190) // #338682, L* 51.2, 4.29:1 on whiteThey share an OKLCH lightness and chroma, but only the coral passes WCAG AA for text on white. WCAG contrast depends only on luminance, and OKLCH's L only comes close to it. CIELab's L* tracks luminance exactly, so we hold L* equal at each stop and use OKLCH for chroma and hue.
Trade-offs and limits#
Equal lightness costs some color: a yellow as dark as a blue reads as olive. What does equal lightness cost? covers each cost, and fitting colors into Display P3 covers colors that screens can't show.
Sources#
- Björn Ottosson, A perceptual color space for image processing (2020), which introduces OKLab.
- W3C, CSS Color Module Level 4, which defines
oklch()and HSL in CSS. - W3C, Web Content Accessibility Guidelines (WCAG) 2.2, which defines relative luminance and contrast ratio.
- The ColorTokensKit source, for how the palette uses L* and OKLCH.
See also
- .contrastRatio(to:method:) Check WCAG 2 and APCA contrast in Swift, and keep your color pairs passing with a unit test.
- How gradients blend and ease Why plain gradients turn gray, how each blend travels between colors, and where easing puts them.
- Replacing your app colors Move backgrounds, text, outlines, states, gradients and charts to tokens, one screen at a time.
- .toOKLCH() Read a color's lightness, chroma and hue the way the palette sees them, to compare or adjust it.