Why Oklab Won: The Science Behind Your CSS Palettes
Björn Ottosson built Oklab in 2020 to fix HSL's perceptual lies. Photoshop, browsers, and W3C tokens followed.
In 2020, Björn Ottosson published a blog post titled “A perceptual color space for image processing” and gave away the math. Four years later, Photoshop made Oklab its default gradient interpolation. Major browsers shipped oklch() in CSS. The Design Tokens Color Module 2025.10 encoded oklab and oklch as first-class token color spaces. Unity gradients and Godot’s color picker adopted the same coordinates. This did not happen because designers grew tired of hex. It happened because HSL and raw sRGB failed measurable perceptual tests and Oklab passed them with simple, fast code.
If your palettes still live in HSL, you are not clinging to tradition. You are calibrating against a coordinate system whose equal steps were never equal in human vision. HSL’s lightness is a geometric remapping of the RGB cube, not a perceptual lightness channel. Yellow at HSL lightness fifty percent dominates layouts while blue at the same number whispers. That is not taste. It is measurable on Ottosson’s plots. OKLCH fixes the coordinate system so token ramps, gradient midpoints, and color-mix() paths behave like designers already assume when they say make this ten percent darker.
This article walks through Ottosson’s requirements, the derivation strategy that traded CAM16 complexity for blend quality, the external validation plots that predict production pain, and a migration story from a team that thought converting hex was enough until hovers proved otherwise. The science is public. The syntax is in CSS. Migration is bookkeeping on coordinates your eyes were already judging honestly without numbers to match.
The requirements HSL could never satisfy
Ottosson listed requirements for a perceptual space suitable for image processing. Those requirements map directly onto design systems even though the original post targeted gradients and image pipelines. He wanted opponent-color structure like CIELAB, separating lightness from chromatic axes so you can darken without hue shift and saturate without lightness drift. He wanted accurate lightness, chroma, and hue prediction so L, C, and H behave orthogonally in practice, not only on paper. He wanted even blends so interpolated paths pass through colors that look between endpoints rather than warmer or muddier detours. He wanted D65 whitepoint alignment with sRGB, Display P3, and Rec. 2020 assumptions in CSS Color 4. He wanted numerical stability with fast, differentiable code and no HDR viewing-condition baggage. He wanted scale invariance so coordinates respond predictably when exposure changes.
HSV and HSL fail essentially all of these in Ottosson’s evaluation. They are widely used but not perceptual. CIELAB and CIELUV improve but mishandle hue, especially in blues. CAM16-UCS predicts well but blends poorly because chroma compression warps interpolation paths. IPT has clean math but weak lightness and chroma prediction. OSA-UCS is accurate but lacks a practical analytical inverse. Ottosson’s bet was to build a space with IPT’s computational structure while optimizing parameters to approximate CAM16-UCS perceptual accuracy without CAM16’s blend artifacts.
For UI palettes, the implication is blunt. Dashboard badges built with HSL sweeps at constant lightness look like yellow screams for attention while blue whispers. OKLCH sweeps at matched L carry peer visual weight because the space was fit to datasets that punish lightness lies. Semantic warning and danger colors need different HSL lightness values to feel equally urgent. In OKLCH you can hold lightness and chroma steadier while rotating hue. Darken utilities that subtract ten percent from HSL lightness do not darken all brand hues by the same perceived amount. OKLCH channel math aligns more closely with what users see even though WCAG contrast still uses relative luminance, not OKLCH L directly.
How Ottosson derived Oklab from CAM16 data
Ottosson trained on three synthetic datasets generated via CAM16 under normal viewing conditions, plus the Ebner-Fairchild hue uniformity data used in IPT’s derivation. Same-lightness pairs tested lightness prediction. Same-chroma pairs tested chroma prediction. Same-hue pairs tested hue prediction against experimental data rather than synthetic ground truth alone. The error metric swapped the coordinate that should match within each pair and measured CIEDE2000 color difference between original and altered pairs. Optimization targeted IPT-structured matrices M₁ and M₂ and exponent γ, forced to one-third in the final model to prevent sRGB gamut folding.
The forward transform, conceptually, linearizes sRGB to an LMS-like cone response via M₁, applies a cube-root nonlinearity on LMS channels with γ equal to one-third, then maps through M₂ to Oklab coordinates L, a, and b. OKLCH is the cylindrical form where C equals the square root of a² plus b² and H equals atan2 of b and a. CSS exposes this as oklch(L C H) per MDN. The inverse is simple enough for real-time shaders and browser style engines. That simplicity mattered as much as the error tables.
Published comparisons showed Oklab approaching CAM16-UCS on lightness and chroma while beating it on practical blend behavior. CAM16-UCS scores artificially well on L and C datasets generated by CAM16 itself because ground truth matches by construction. Oklab’s job was to approach CAM16 lightness and chroma predictions while beating it on hue stability and paths that do not desaturate prematurely when mixing white into blue. On hue RMS, IPT edges Oklab slightly in Ottosson’s table. Oklab wins the practical trade with better L and C, simple inverse, stable sRGB gamut behavior, and blend paths designers recognize from Photoshop’s default gradient mode.
Ottosson’s hue sweep plots remain the most persuasive artifact for working teams. Side by side, Oklab at constant lightness and chroma shows even visual weight across the spectrum. HSV at constant value and saturation balloons yellow, magenta, and cyan that look far lighter than red and blue because HSV value is not perceptual lightness. He then plots Oklab-predicted lightness of the HSV sweep and the curve spikes at yellow and cyan, proving the non-uniformity is measurable, not subjective taste.
/* HSL — same L, different perceived weight */
.hsl-sweep {
--red: hsl(0 100% 50%);
--yellow: hsl(60 100% 50%);
--blue: hsl(240 100% 50%);
}
/* OKLCH — same L, matched perceived weight */
.oklch-sweep {
--red: oklch(0.63 0.26 25);
--yellow: oklch(0.63 0.17 95);
--blue: oklch(0.63 0.20 250);
}
CAM16-UCS remains the perceptual uniform space associated with CIECAM02. Ottosson credits it as the best overall perceptual predictor in his survey while rejecting it for image processing because chroma compression makes white-to-blue blends desaturate too quickly, numerical complexity and viewing-condition parameters make it impractical in CSS and GPU shaders, and HDR-oriented spaces introduce scale dependence that conflicts with one token value in many contexts. Oklab’s white-to-blue blend plot stays saturated longer, then transitions smoothly, which is the behavior designers expect when lightening a brand blue toward white without washing through gray-purple. That geometry is why OKLCH gradient interpolation avoids the gray dead zone described in Gradients Without the Gray Dead Zone.
When converting hex was not the same as migrating geometry
The gap between theory and production showed up at a healthcare SaaS company with a mature Figma library and a compliance culture that treated accessibility evidence as release infrastructure. The design system team read Ottosson’s post in 2021 and again when Photoshop adopted Oklab defaults. Leadership approved an OKLCH migration framed as a coordinate conversion project. Every approved hex and HSL value would map through CSS Color 4 algorithms to OKLCH equivalents that matched visually. CI would enforce OKLCH in token JSON. The project plan had no line item for rebuilding derivation logic because nobody counted how many utilities still said darken by subtracting HSL lightness.
Phase one succeeded on paper. Primitives converted with tight visual matches. Semantic aliases updated. Storybook swatches looked identical before and after in side-by-side screenshots. The team announced migration complete in an internal changelog. Within two weeks, support tickets described buttons that felt mushy on hover and chart legends that looked uneven despite official tokens living in OKLCH notation.
Investigation followed the bugs to component utilities, not to the token file. A shared button mixin still computed hover backgrounds with hsl(from var(--accent) h s calc(l - 8%)) because the mixin predated the token rename and nobody had searched for HSL usage in Sass. The accent token was OKLCH under the hood after conversion, but the relative HSL math ran on converted values through a path that reintroduced hue-dependent darkening. Cobalt accents darkened perceptually on hover. Yellow-green accents barely changed. Nurses scanning medication tables noticed the inconsistency before engineering did because the UI broke rhythm on dense screens where hover affordance matters.
Chart legends failed for a different reason. A data visualization engineer had built a small script that generated ten tints by linearly stepping HSL lightness from a base hex exported from Figma. The base hex was now a round-trip conversion from OKLCH, but the script still stepped HSL lightness because the comment at the top said brand ramp generator and nobody had read the implementation since 2022. The legend looked fine for blue series and wrong for amber series. Design-ops reopened the migration as a geometry migration, not a notation migration.
The second phase replaced every lighten, darken, and saturate utility with OKLCH relative syntax or explicit OKLCH coordinates derived from anchored steps. Hover darkened with calc(l - 0.06) on the same hue and chroma unless brand guidelines required chroma taper on dark fills. Tint generation for charts moved to color-mix(in oklch, …) with documented weights. Contrast failures surfaced that HSL’s lies had hidden. Muted text pairs that passed hex spot checks failed when OKLCH honesty made the lightness delta visible against raised surfaces. Fixing tokens beat patching components because the compliance team wanted evidence at the source.
External validation plots predicted exactly these failures before the team felt them. Ottosson’s Munsell renotation data plots show Oklab and CAM16-UCS forming near-circular chroma rings while CIELAB and HSV squash and stretch rings, indicating chroma predictions that drift with hue. Luo-Rigg ellipses from experimental just-noticeable-difference studies show Oklab tracking perceptual uniformity without CAM16’s extreme chroma compression. HSV breaks outside naive assumptions. Those plots are not academic wallpaper. They predict HSL ramps that bunch at the ends, yellow semantic colors that need manual exceptions, and chart palettes where step five hundred means different things per hue.
Training and documentation changed after phase two. New hires learn OKLCH as the authoring space and learn that notation without geometry is an empty victory. The compliance packet now includes a short section on interpolation space for gradients and mixes because procurement reviewers started asking how midpoints are computed, not only whether contrast ratios pass. The migration took longer than the conversion spreadsheet predicted and shorter than a full redesign would have taken.
From Photoshop defaults to token infrastructure
Photoshop’s adoption of Oklab as default gradient interpolation is the professional signal that blend quality matters outside browser Twitter discourse. Gradient interpolation is exactly the test Ottosson optimized. Paths between endpoint colors must look like even transitions. A gradient authored in OKLCH in CSS and a gradient authored in Oklab in Photoshop are philosophically consistent. Handoff arguments about muddy hero banners shrink when both sides interpolate in the same perceptual space.
The W3C CSS Color Module Level 4 specifies oklab() and oklch() syntax, conversion algorithms, and interpolation rules including the none keyword for missing hue components during interpolation. CSS Color Module Level 5 extends tooling with color-mix() and relative color syntax on the same coordinates. Browser support made OKLCH a viable authoring space rather than a progressive-enhancement curiosity. The Design Tokens Color Module 2025.10 lists oklab and oklch alongside srgb, display-p3, hsl, and others, with conversion math referenced to CSS Color 4. Token JSON can store components directly.
{
"brand": {
"accent": {
"$type": "color",
"$value": {
"colorSpace": "oklch",
"components": [0.58, 0.17, 250],
"hex": "#3b5bdb"
}
}
}
}
Oklab is no longer a blog post trick. It is infrastructure shared by creative tools, browsers, and token validators. Teams still must handle organizational naming debt. A token named blue-600 is still a primitive leak whether the value is hex or oklch coordinates. Oklab does not fix semantics. It fixes the physics underneath semantics.
Ottosson is explicit about scope. Oklab targets normal viewing conditions, not a full appearance model for every HDR environment. Out-of-gamut behavior follows CSS gamut mapping rules. User testing and contrast math remain required. Wide gamut Display P3 greens still need explicit authoring. Perceptual uniformity does not imply infinite gamut. Expect surfaced contrast failures when honesty replaces HSL’s comfortable lies. Fixing tokens is cheaper than patching components indefinitely.
When you write oklch(0.65 0.18 264), you inherit a space derived from CAM16-trained datasets, validated against hue uniformity experiments, and optimized so white-to-blue blends do not lie. When you write hsl(220 90% 56%), you inherit a cylindrical remapping of the RGB cube that makes yellow look like a spotlight at the same lightness number. The science is public at bottosson.github.io/posts/oklab/. The syntax is in CSS. The tokens spec caught up in 2025.10.
No single benchmark crowned Oklab. Synthetic lightness and chroma fit, hue ellipses, Munsell rings, blend plots, and shipping adoption in browsers and Photoshop converged on the same answer because the bundle of constraints mattered more than any one score. Teams that treat OKLCH as a notation swap miss the point. Teams that rebuild derivation and interpolation in the same space inherit the win Ottosson documented for image processing and that design systems needed all along.
Migration is bookkeeping on coordinates your eyes already judged honestly without numbers to match. Start by converting approved values, then hunt every HSL and sRGB interpolation path still hiding in mixins, chart scripts, and gradient definitions. The ramp exercise in Your HSL Palette Lies to You remains the fastest way to convince skeptics in your org because side-by-side swatches argue louder than specification links. After that, the work is discipline, not persuasion.