Practical Guides

Recipes: building palettes, checking contrast, and shipping OKLCH in CSS.

Everything before this chapter was understanding; this one is execution. Four recipes cover the work color systems actually demand: building a scale, auditing it, shipping it, and migrating an old one.

Recipe: build a 10-step scale#

A design-system scale (the blue-100 blue-1000 family) is a walk through OKLCH space:

  • 1 - Fix the hue. Pick your brand hue angle (say H = 250 for blue) and hold it constant. Optionally drift a few degrees toward the light end to counteract perceived hue shift.
  • 2 - Ramp lightness evenly. Distribute L from ~0.98 down to ~0.24. Even steps in OKLCH L are even to the eye - this is the whole trick.
  • 3 - Shape chroma as a bell.Chroma physically can't stay high near white or black; peak it in the middle (C ≈ 0.12–0.18) and taper both ends (C ≈ 0.02–0.06).
  • 4 - Verify.The Lab's Lightness Progression should hug the ideal line; the Distance Matrix should show neighbouring ΔE ≥ ~7; Gamut Coverage should be clean for sRGB.
100
200
300
400
500
600
700
800
900
1000
A 10-step scale generated by this recipe: constant hue 250°, even lightness ramp, bell-shaped chroma.

The Lab's generation methods automate exactly this: “Balanced” applies an eased lightness curve with Gaussian chroma; “Base Respecting” keeps your exact hue and chroma and ramps only lightness.

Recipe: audit a palette#

  • 1 - Contrast every text pairing. Body text needs WCAG 4.5:1 (AA); check both white-on-color and color-on-white for each step. In the Lab, the Contrast Analysis card computes both plus APCA Lc.
  • 2 - Find the flip point. Identify the step where text color must switch between dark and light (usually near L ≈ 0.65–0.7) and encode it as a rule, not a per-case choice.
  • 3 - Run the CVD simulations. If two adjacent steps merge under deuteranopia, their roles (e.g. chart series) need more lightness separation.
  • 4 - Grayscale sanity check.Toggle the Palettes card's grayscale preview: the ramp should still read as an ordered sequence.

Recipe: ship OKLCH in CSS#

All evergreen browsers parse oklch() (Chrome 111+, Safari 16.4+, Firefox 113+ - spring 2023). For anything older, ship a hex fallback the old-fashioned way - earlier declarations win when the newer syntax is unknown:

tokens.css
:root {
  /* Fallback first, modern value second */
  --brand: #3577c7;
  --brand: oklch(0.58 0.14 250);
}

.button {
  background: var(--brand);
}
.button:hover {
  /* Derived states stay one perceptual step away */
  background: oklch(from var(--brand) calc(l - 0.06) c h);
}

/* Wide-gamut bonus for capable displays */
@media (color-gamut: p3) {
  :root { --brand: oklch(0.58 0.19 250); }
}
  • Keep tokens in OKLCH and let build tooling emit hex fallbacks (PostCSS @csstools/postcss-oklab-function does this automatically).
  • Name tokens by role (--surface, --on-surface) rather than by color, so dark mode is a re-assignment, not a rewrite.

Recipe: migrate a hex palette#

  • 1 - Convert losslessly. Paste the palette into the Converter (it handles hex/RGB/HSL/OKLCH in bulk, preserving CSS variable names). Conversion changes notation, not color.
  • 2 - Read the diagnosis. Load the converted palette in the Lab. Legacy hand-picked ramps typically show a sagging lightness curve and chaotic hue drift - now visible as data.
  • 3 - Regularize gently.Snap each step's L to an even ramp and unify hue drift, keeping chroma close to the original. The result usually differs by ΔE < 5 per step - invisible individually, dramatically more consistent as a system.
  • 4 - Diff before adopting. Compare old and new in grayscale and in the contrast matrix; the regularized scale should only improve both.

Further reading#