Modern Color with OKLCH

Engineering
Share on LinkedIn Share on X Share on Reddit Share on HN Share on Bluesky

title: "Modern Color with OKLCH" slug: "web-color-functions-oklch" description: "Use OKLCH color in CSS for perceptually uniform palettes: syntax, comparison with HSL and LCH, wide-gamut support, and building accessible color systems." datePublished: "2026-03-15" dateModified: "2026-07-17" tags:



title: "web-color-functions-oklch" slug: "web-color-functions-oklch" description: "" datePublished: "2026-07-17" dateModified: "2026-07-17" tags:



title: "web-color-functions-oklch" slug: "web-color-functions-oklch" description: "" datePublished: "2026-07-17" dateModified: "2026-07-17" tags:



title: "web-color-functions-oklch" slug: "web-color-functions-oklch" description: "" datePublished: "2026-07-17" dateModified: "2026-07-17" tags:



title: "web-color-functions-oklch" slug: "web-color-functions-oklch" description: "" datePublished: "2026-07-17" dateModified: "2026-07-17" tags:



title: "web-color-functions-oklch" slug: "web-color-functions-oklch" description: "" datePublished: "2026-07-17" dateModified: "2026-07-17" tags:



title: "Modern Color with OKLCH" slug: "web-color-functions-oklch" description: "Use OKLCH color in CSS for perceptually uniform palettes: syntax, comparison with HSL and LCH, wide-gamut support, and building accessible color systems." datePublished: "2026-03-15" dateModified: "2026-07-17" tags:



I generated a palette in HSL: ten hues at 50% saturation and 50% lightness. Yellow screamed. Blue whispered. Purple looked fine. The palette was mathematically consistent and visually a mess. HSL's lightness axis doesn't match human perception — it's based on the underlying RGB model, not how bright colors actually look. OKLCH fixes this by using a perceptually uniform space where equal L values look equally bright regardless of hue. It's the color format CSS has needed for decades, and it's finally usable in production.

OKLCH syntax

.element {
  color: oklch(0.62 0.19 250);    /* lightness chroma hue */
  background: oklch(0.95 0.02 250);
  border-color: oklch(0.45 0.15 250);
}

Optional alpha: oklch(0.62 0.19 250 / 0.5)

Perceptual uniformity in practice

Same lightness, different hues — all look equally bright:

--red:    oklch(0.65 0.2 25);
--orange: oklch(0.65 0.2 65);
--yellow: oklch(0.65 0.2 100);
--green:  oklch(0.65 0.2 145);
--blue:   oklch(0.65 0.2 250);
--purple: oklch(0.65 0.2 300);

In HSL, hsl(60, 100%, 50%) (yellow) looks neon while hsl(240, 100%, 50%) (blue) looks dark. In OKLCH, both at L=0.65 look like they have the same visual weight.

Building a color scale

Generate a systematic palette by varying lightness with fixed chroma and hue:

:root {
  --blue-50:  oklch(0.97 0.02 250);
  --blue-100: oklch(0.93 0.04 250);
  --blue-200: oklch(0.87 0.08 250);
  --blue-300: oklch(0.78 0.12 250);
  --blue-400: oklch(0.68 0.16 250);
  --blue-500: oklch(0.58 0.19 250);
  --blue-600: oklch(0.50 0.18 250);
  --blue-700: oklch(0.42 0.16 250);
  --blue-800: oklch(0.35 0.13 250);
  --blue-900: oklch(0.28 0.10 250);
}

Each step is visually equidistant. In HSL, you'd need to hand-tune every step because the perceptual distance between 90% and 80% lightness varies by hue.

Comparison with other color formats

Format Perceptually uniform Wide gamut Browser support
Hex/RGB No No Universal
HSL No No Universal
LCH Yes Yes Modern browsers
OKLCH Yes (better than LCH) Yes Modern browsers
Display P3 N/A Yes Safari, Chrome
color-mix() Depends on space Depends Modern browsers

OKLCH improves on LCH by using a better lightness model (Oklab) that handles blue hues more accurately. LCH's lightness is distorted in the blue region, which is why OKLCH is preferred for CSS.

Wide-gamut colors

On P3 displays, OKLCH can render colors more vivid than sRGB allows:

.vivid {
  /* This blue is more saturated than any sRGB hex can express */
  color: oklch(0.55 0.25 250);
}

The browser clamps to the display's available gamut. On sRGB screens, it renders the closest sRGB equivalent. On P3 screens, you get the full saturation. One definition, adaptive rendering.

Check gamut support:

@supports (color: oklch(0 0 0)) {
  .accent { color: oklch(0.55 0.25 250); }
}
@supports not (color: oklch(0 0 0)) {
  .accent { color: #2563eb; } /* sRGB fallback */
}

Accessible contrast with OKLCH

Because L is perceptually uniform, contrast ratios are predictable from lightness values:

/* Text on background: aim for L difference ≥ 0.4 for WCAG AA */
--text:       oklch(0.20 0.02 250);  /* dark text */
--background: oklch(0.98 0.01 250);  /* light background */
/* L difference: 0.78 → passes AA and AAA */

Generate accessible pairs programmatically:

function contrastPair(hue, chroma) {
  return {
    text:       `oklch(0.20 ${chroma} ${hue})`,
    background: `oklch(0.97 ${chroma * 0.3} ${hue})`,
    accent:     `oklch(0.55 ${chroma} ${hue})`,
    accentText: `oklch(0.98 0.01 ${hue})`,
  };
}

color-mix with OKLCH

Blend colors in perceptual space:

.element {
  /* Mix blue and white in OKLCH for a perceptually correct tint */
  background: color-mix(in oklch, oklch(0.55 0.2 250) 25%, white);
}

color-mix(in srgb, ...) produces muddy midpoints. color-mix(in oklch, ...) produces clean tints and shades.

Migrating from HSL/hex

Convert existing colors with DevTools or tools:

  1. Chrome DevTools color picker supports OKLCH — paste a hex value and read the OKLCH equivalent
  2. oklch.com — interactive OKLCH picker with gamut visualization
  3. Build new palettes in OKLCH; keep hex as fallback
:root {
  --primary: #3b82f6;                          /* fallback */
  --primary: oklch(0.62 0.19 250);             /* override */
}

The cascade handles fallback automatically — browsers that don't understand OKLCH use the hex.

Token migration checklist

Convert palette with @csstools/postcss-oklab-function; ship one release with hex fallback inside @supports not (color: oklch(0 0 0)). Re-run APCA contrast on components that passed WCAG under hex — OKLCH identical coordinates can fail on colored backgrounds because perceptual lightness shifted. Document P3-only brand colors with sRGB fallback token for Windows laptops without wide gamut.

Token migration checklist

Convert palette with @csstools/postcss-oklab-function; ship one release with hex fallback inside @supports not (color: oklch(0 0 0)). Re-run APCA contrast on components that passed WCAG under hex — OKLCH identical coordinates can fail on colored backgrounds because perceptual lightness shifted. Document P3-only brand colors with sRGB fallback token for Windows laptops without wide gamut.

Design token migration checklist

  1. Export Figma styles to OKLCH tokens (Figma supports OKLCH natively in 2025+)
  2. Replace :root CSS variables in one PR per app shell
  3. Run visual regression on marketing + app chrome
  4. Update Storybook swatches for designers
  5. Document token naming: --color-brand-500 not --blue

Print and forced-colors modes

@media print {
  :root { --brand: oklch(30% 0.05 250); }
}

@media (forced-colors: active) {
  .button { border: 2px solid ButtonText; }
}

OKLCH does not exempt you from Windows High Contrast—test forced-colors separately.

OKLCH in Canvas and WebGL

CSS OKLCH does not automatically propagate to canvas—convert with color.js if drawing charts programmatically for consistent brand colors between DOM and canvas layers.

Resources

Failure modes specific to web color functions oklch

Performance work on web color functions oklch must prioritize field metrics (CrUX / RUM) over lab vanity. Lab still helps for debugging, but ship decisions should key off p75 LCP, INP, and CLS on real devices.

For web color functions oklch:

A useful ritual: every sprint, pick the worst URL in CrUX for your template and run a focused fix with a before/after RUM chart.

Signal Target Alarm
Cold start p95 Team-defined SLO Page on burn rate
Throttle count Baseline − noise Ticket if sustained
Downstream timeouts Budget cap Weekly review

Ownership and on-call for web color functions oklch

Reviewers should challenge assumptions encoded in web color functions oklch: defaults copied from tutorials, timeouts that exceed upstream SLAs, and authz checks applied only on the primary UI path. Require a short threat or failure note in the PR when the change touches a trust boundary.

Concrete probes:

  1. Scenario C for web color functions oklch: traffic 3× baseline — prove autoscaling or shedding keeps the golden journey healthy.
  2. Scenario A for web color functions oklch: partial dependency outage — prove clients degrade gracefully and retries do not amplify load.
  3. Scenario B for web color functions oklch: bad config shipped — prove rollback within the declared RTO without data corruption.

Rollout sequence that worked for web color functions oklch

Roll out web color functions oklch behind a flag or weighted route when possible. Start with internal users or a low-risk geography. Watch the signals in the table for at least one full business cycle before calling the migration done. Keep the previous path warm until error budgets stabilize.

Document the owner, the dashboard, and the single command that reverts the change. If that sentence is hard to write, the design is not ready for production traffic.

Compliance evidence for web color functions oklch

Detail 1 (191): for web color functions oklch, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When compliance evidence for web color functions oklch becomes painful, it is usually because that contract was implicit.

I keep a short matrix: who can break web color functions oklch, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.

If you only remember one thing about web color functions oklch: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.

Developer experience when changing web color functions oklch

Detail 2 (222): for web color functions oklch, define the contract between producers and consumers explicitly — payload shape, timeout, and idempotency key. When developer experience when changing web color functions oklch becomes painful, it is usually because that contract was implicit.

I keep a short matrix: who can break web color functions oklch, how we detect it within five minutes, and who is paged. Update the matrix when ownership moves. Add one synthetic check that exercises the failure path, not only the happy path. Prefer checks that run continuously over quarterly manual reviews that everyone skips under deadline pressure.

If you only remember one thing about web color functions oklch: optimize for reversible decisions. Reversibility beats cleverness when the incident channel is busy and the blast radius is unclear.

Frequently asked questions

What is the main production risk with web color functions oklch?

Teams ship without field measurement—web color functions oklch failures appear as silent UX regressions, cost drift, or audit findings rather than clear errors.

When should we prioritize web color functions oklch?

Prioritize when user research, CrUX, support tickets, or compliance requirements show pain on critical paths—not when a checklist mentions it abstractly.

How do we validate web color functions oklch changes?

Baseline RUM before changes, compare p75 after deploy, and keep rollback via feature flags or cache purge documented in the PR.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →