01

Use OKLCH when its coordinates match the palette job

OKLCH is the polar form of Oklab. L represents perceptual lightness, C is chroma, and H is a hue angle. CSS Color 4 describes Oklab as optimized for visually similar colors and notes improvements in hue linearity, perceptual uniformity, and chroma prediction compared with CIE LCH. Those properties make OKLCH a useful working space for arranging related tokens, especially when a palette needs a lightness ramp or interpolation that does not collapse toward gray as readily as rectangular mixing can. The model is improved, not magically perfect: every step still needs visual review on real displays.

  • Use L to organize lighter and darker roles while keeping in mind that equal numeric steps are only an approximation of equal visual steps.
  • Use C to control distance from the neutral axis; larger values are more colorful until the chosen output gamut becomes the constraint.
  • Use H to keep a family near one hue or to choose an explicit path between hues during interpolation.
  • Treat L, C, and H as palette coordinates, not accessibility claims. WCAG contrast uses relative luminance derived from the final rendered sRGB colors.
  • Record the intended gamut and fallback alongside the OKLCH source so a token remains reproducible across browsers, design tools, and displays.
02

Write the syntax without the chroma percentage trap

Modern OKLCH syntax uses spaces and an optional slash before alpha: oklch(L C H / A). Commas are invalid. Lightness may be a number from 0 to 1 or a percentage from 0% to 100%. Chroma is normally written as a number; CSS Color 4 says a chroma percentage maps 100% to 0.4, so 25% means 0.1 rather than 0.25. Chroma has no single universal maximum: values around 0.5 cover the practical range, while the usable boundary depends on lightness, hue, and target gamut. Negative chroma is clamped to zero.

css
:root {
  --brand: oklch(70% 0.16 30);
  --brand-same-chroma: oklch(70% 40% 30); /* 40% of 0.4 = 0.16 */
  --brand-muted: oklch(70% 0.08 30);
  --brand-overlay: oklch(70% 0.16 30 / 65%);
}
CoordinateCSS formPractical meaningImportant boundary
L70% or 0.7Perceptual lightness coordinateNot WCAG relative luminance
C0.16 or 40%Distance from the neutral axis100% maps to 0.4
H30 or 30degPolar hue anglePowerless when chroma is effectively zero
A/ 65%Opacity before compositingContrast depends on the backdrop
03

Build a lightness ramp and test every foreground

The following ramp holds chroma at 0.08 and hue at 30 while changing L from 35% to 85%. The HEX values are 8-bit sRGB approximations calculated from the CSS Color 4 Oklab matrices and rounded to the nearest channel value. Contrast is then recalculated from each rounded HEX using the WCAG relative-luminance formula. AA for normal text requires at least 4.5:1. This separation matters: OKLCH L helps structure the ramp, while the final composited color supplies the accessibility evidence.

OKLCH8-bit sRGBBlack textWhite textAA normal choice
oklch(35% 0.08 30)#5D28201.79:111.73:1White
oklch(45% 0.08 30)#7C433A2.72:17.72:1White
oklch(55% 0.08 30)#9B5F554.16:15.05:1White
oklch(65% 0.08 30)#BB7D726.29:13.34:1Black
oklch(75% 0.08 30)#DC9B909.13:12.30:1Black
oklch(85% 0.08 30)#FEBBAF12.94:11.62:1Black
  • The foreground switches between L 55% and L 65% for this particular hue and chroma; there is no universal OKLCH lightness cutoff.
  • L 55% does not make black pass AA for normal text even though it looks like the middle of the ramp; the measured ratio is 4.16:1.
  • Keep unrounded ratios for pass/fail decisions and round only the values shown in documentation.
  • Test hover, selected, disabled, gradient, and translucent states after all layers are composited.
  • An approximate HEX export is useful for sRGB review, but it cannot exactly encode an original color that lies outside sRGB.
04

Increase chroma until the target gamut becomes the constraint

Chroma cannot be assigned one safe maximum for every palette. At fixed L 70% and H 30, values through C 0.19 remain inside sRGB in the calculation below. C 0.20 is already outside sRGB while still inside Display P3, and C 0.24 remains a valid OKLCH color that this wider gamut can contain. A different lightness or hue reaches the boundary elsewhere. For rows outside sRGB, no exact 8-bit HEX exists; replacing them with a clipped HEX would hide the gamut change and may alter hue or lightness.

OKLCH at L 70%, H 308-bit sRGB approximationsRGBDisplay P3Interpretation
C 0.00#9E9E9EInsideInsideNeutral; hue is powerless
C 0.08#CB8C81InsideInsideMuted color
C 0.16#F17260InsideInsideStrong sRGB color
C 0.19#FE6652InsideInsideNear this sRGB boundary
C 0.20OutsideInsideNo exact HEX representation
C 0.24OutsideInsideWide-gamut result
05

Separate syntax fallback from gamut mapping

Fallback and gamut mapping solve different problems. A fallback gives older software a declaration it understands. A supporting browser can parse the OKLCH declaration yet still need to map that color to the actual display gamut. Use @supports when a custom property must remain valid as a whole: placing an unsupported color only in the custom property can leave the declaration valid until substitution and cause the consuming property to fail rather than use an earlier value. The conditional block below avoids that trap.

css
:root {
  --brand: #FE6652;
}

@supports (color: oklch(70% 0.20 30)) {
  :root {
    --brand: oklch(70% 0.20 30);
  }
}

.button {
  color: #000000;
  background: var(--brand);
}
  • Choose the fallback deliberately; #FE6652 is the last in-sRGB step in this example, not a claim that it is identical to C 0.20.
  • State whether the preferred token targets sRGB, Display P3, or another gamut, and test on displays that represent each path.
  • Do not pre-clip every token to make tools agree. That discards the wide-gamut source and can create uneven ramps.
  • Expect the browser or color pipeline to gamut-map at output time; inspect the rendered result rather than assuming a specific algorithm forever.
  • Recalculate foreground contrast for the fallback and for the mapped preferred color instead of inheriting one ratio from the source coordinates.
06

Choose the interpolation space and hue path deliberately

The space used between endpoints is as important as the endpoint notation. CSS gradients and color-mix() can request OKLCH interpolation. A polar space can preserve chroma better than mixing rectangular channels through a dull midpoint, but hue has multiple routes around its circle. The shorter and longer paths may produce entirely different intermediate families; increasing and decreasing paths specify direction. Intermediates can also leave the output gamut even when both endpoints are displayable, so the finished ramp still needs gamut and contrast checks.

css
.spectrum {
  background: linear-gradient(
    90deg in oklch shorter hue,
    oklch(70% 0.16 30),
    oklch(70% 0.16 280)
  );
}

:root {
  --brand-soft: color-mix(
    in oklch,
    oklch(70% 0.16 30) 35%,
    white
  );
}
  • Use Oklab when a straight perceptual interpolation is desired and OKLCH when controlling a polar hue path or retaining chroma is more important.
  • Specify shorter, longer, increasing, or decreasing hue when the route changes product meaning; do not rely on an accidental default for a semantic scale.
  • Sample intermediate stops during review. Endpoint checks alone cannot reveal an out-of-gamut or low-contrast midpoint.
  • Remember that mixing with white changes more than L in a polar model. If only one coordinate should vary, generate explicit OKLCH tokens instead.
  • Keep derived colors in the same documented pipeline so design previews, CSS output, screenshots, and audits use the same interpolation rules.
07

Ship OKLCH tokens with auditable evidence

A production OKLCH palette should be reviewable without reconstructing hidden design-tool decisions. Store the source coordinates at useful precision, identify the target gamut, keep an intentional fallback, and export calculated evidence beside each semantic token. Then test the actual rendered states. This preserves the perceptual advantages of the model while making browser support, gamut behavior, and accessibility ordinary engineering inputs instead of late surprises.

css
:root {
  --action-strong: oklch(55% 0.08 30);
  --action: oklch(65% 0.08 30);
  --action-soft: oklch(85% 0.08 30);

  --on-action-strong: #FFFFFF; /* 5.05:1 on #9B5F55 */
  --on-action: #000000;        /* 6.29:1 on #BB7D72 */
  --on-action-soft: #000000;   /* 12.94:1 on #FEBBAF */
}
  • Keep L distinct from WCAG relative luminance and keep C distinct from HSL saturation.
  • Document percentage normalization, especially that C 100% equals 0.4 in CSS syntax.
  • Treat hue as powerless for neutral colors and avoid unstable labels based on an invisible coordinate.
  • Record which tokens are inside sRGB, which require a wider gamut, and which fallback is served when the preferred path is unavailable.
  • Retain enough precision for conversion, but publish rounded HEX only as an identified 8-bit sRGB approximation.
  • Test final composited colors for contrast; alpha, gradients, overlays, and gamut mapping can all change the pair users see.
  • Review interpolation midpoints, focus indicators, borders, hover states, selected states, light theme, dark theme, and forced-color behavior separately.
  • Link the source specification and conversion method so future token-generator changes remain explainable and reproducible.
S

Primary sources

The factual claims in this article are checked against these published specifications.

  1. W3C CSS Color 4 — Oklab and OkLCh overview
  2. W3C CSS Color 4 — specifying Oklab and OkLCh
  3. W3C CSS Color 4 — gamut mapping
  4. W3C CSS Color 4 — color interpolation methods
  5. W3C CSS Images 4 — gradient color interpolation
  6. W3C CSS Color 5 — color-mix()
  7. W3C CSS Conditional 3 — @supports
  8. W3C WCAG 2.2 — relative luminance
  9. W3C WCAG 2.2 — Contrast (Minimum)