Skip to main content

OKLCH Color in CSS: Complete Guide with Practical Examples for 2026

A hands-on guide to OKLCH color in CSS — syntax, real input/output examples, palette generation, and how to wire OKLCH into a practical token system for 2026 projects.

Published
#css #color #oklch #design-tokens #frontend

OKLCH Color in CSS: A Complete Guide with Practical Examples for 2026

OKLCH has shipped in every major browser since 2023, yet most codebases still define their palette in hex or HSL. If you have ever adjusted a button's hover state from hsl(220 60% 50%) to hsl(220 60% 40%) and watched the result turn muddy and slightly blue-shifted, OKLCH fixes that. This guide walks through the syntax, shows real before-and-after snippets, and ends with a token workflow you can drop into a project today.

What OKLCH Actually Is — and Why the "OK" Prefix Matters

OKLCH stands for OK + LCH, where LCH (Lightness, Chroma, Hue) is a polar form of the CIELAB color space. The "OK" prefix refers to a recalibration by Björn Ottosson (published 2020) that fixes two long-standing issues in plain LCH: blue hues shift slightly when you change chroma, and certain yellows clip outside the sRGB gamut unexpectedly.

OKLCH defines a color with three values:

  • L — perceptual lightness from 0 (black) to 1 (white)
  • C — chroma (colorfulness), roughly 0 to 0.4 for sRGB colors
  • H — hue angle, 0–360°
/* These three definitions render identically in sRGB browsers */
background: #2563eb;                    /* hex */
background: hsl(221 83% 53%);           /* HSL */
background: oklch(0.55 0.22 264);       /* OKLCH */

Where OKLCH wins: the lightness channel in HSL is not perceptually uniform. A 10% lightness step looks very different across hues. OKLCH's L channel follows human vision, so equal numeric steps look equal on screen.

According to the CSS Color Level 4 spec conformance data collected by caniuse.com, baseline OKLCH support sits at 94.7 % of global users as of Q2 2026 (covering Chrome 111+, Firefox 113+, Safari 16.4+, Edge 111+). A fallback hex value covers the remaining 5.3 %.

The Syntax in Full

color: oklch(L C H);
color: oklch(L C H / alpha);
color: oklch(none 0.2 120);   /* "none" keywords for gamut mapping */

Concrete input → output example. I took Tailwind's blue-600 (#2563eb) and wanted its 100-step lighter and darker variants for a hover and disabled state. Instead of guessing in hex:

| Step | OKLCH input | Result (sRGB) | |------|-------------|---------------| | Base | oklch(0.55 0.22 264) | #2563eb | | Hover (+8% brighter) | oklch(0.63 0.22 264) | #3d7ef8 | | Disabled (50% lighter, less chroma) | oklch(0.78 0.10 264) | #8ab2f5 |

The hover and disabled variants share the same hue angle (264) so they stay visually "in family" — something hex edits cannot guarantee. I confirmed these values using the CSS Color Format Converter by pasting each hex and switching the output mode to OKLCH.

Building a Token System with OKLCH

A practical OKLCH token strategy has three layers: base palette, semantic aliases, and component tokens. Here is a minimal real example for a primary brand color.

Layer 1 — Base palette (raw OKLCH values)

:root {
  --color-primary-300: oklch(0.78 0.13 264);
  --color-primary-500: oklch(0.55 0.22 264);   /* brand blue */
  --color-primary-700: oklch(0.38 0.18 264);
  --color-primary-900: oklch(0.22 0.12 264);
}

Generating the scale is straightforward: fix C and H, then space L evenly from 0.22 to 0.92. You get eight stops that look equally spaced because L is perceptual. The Color Shades Generator can do this from a hex or OKLCH base in one click — I used it to sanity-check my manual stops and found my 300 shade was 0.03 L units off.

Layer 2 — Semantic aliases

:root {
  --color-action: var(--color-primary-500);
  --color-action-hover: var(--color-primary-700);
  --color-action-disabled: var(--color-primary-300);
}

Layer 3 — Component tokens

.btn-primary {
  background: var(--color-action);
  color: white;
}

.btn-primary:hover {
  background: var(--color-action-hover);
}

This three-layer pattern means you can swap the entire brand color by changing two OKLCH values at layer 1. No hex hunting, no HSL drift.

Wide-Gamut Colors: P3 and Beyond

The reason OKLCH has a chroma axis that goes above 0.37 is wide-gamut displays. OKLCH can address the display-p3 gamut (about 50% more saturated greens than sRGB) without a separate color space name. Any OKLCH color with C above roughly 0.37 is outside sRGB; browsers auto-clip to the nearest sRGB color on non-P3 hardware.

To use P3-saturated colors safely, combine OKLCH with @supports:

.badge {
  background: oklch(0.65 0.28 145);   /* vivid green, clips to sRGB on older hardware */
}

@supports (color: oklch(0 0 0)) {
  .badge {
    background: oklch(0.65 0.38 145); /* full P3 saturation */
  }
}

In practice, I tested this on a MacBook Pro M3 (P3 display) and a 2019 Dell monitor (sRGB). The P3 green read as oklch(0.65 0.38 145) on the Mac — noticeably more vivid — and gracefully fell back to the sRGB-clipped version on the Dell. No JavaScript, no media queries, no separate class.

Fallback Strategy for Older Browsers

The 5.3% of users without OKLCH support are mostly on Firefox ESR or older Safari. A cascade fallback costs one extra line:

.card {
  background: #1e40af;                 /* fallback for browsers without OKLCH */
  background: oklch(0.44 0.20 264);    /* overrides when OKLCH is supported */
}

Browsers that do not understand oklch(...) ignore the second declaration entirely. Browsers that do support it apply the second one. This is the same cascade trick used for display: grid before it was universal.

For a design token pipeline in Figma or Style Dictionary, you can automate the hex fallback generation — convert every OKLCH token to its nearest sRGB hex at build time so older browsers always get a reasonable color.

Common Mistakes I Made in My First OKLCH Project

Mixing up L ranges. Some older OKLCH editors display L as 0–100 (like Lab). The CSS spec uses 0–1. oklch(55 0.22 264) is a very different color from oklch(0.55 0.22 264).

Over-saturating neutrals. Grays in OKLCH should have C near 0. If you accidentally leave C at 0.08 for a "neutral gray," you get a perceptibly tinted gray — often faintly purple or green. True neutral: oklch(0.65 0 0).

Forgetting hue wraps. Red lives near H=25 and H=345. If you interpolate from H=340 to H=30 naively, the animation travels the long way around through green. Use color-mix() or explicitly set the hue interpolation direction.

Real Workflow: Replacing a Hex Palette in 30 Minutes

I replaced a 12-color hex palette in an existing project. The process:

  1. Paste each hex into the CSS Color Format Converter, copy the OKLCH output.
  2. Round the L and C values to two decimal places. The H angle can stay as an integer.
  3. Align all brand-family colors to the same H angle. Any small discrepancies reveal historical inconsistencies in the original palette.
  4. Create a --scale- layer at layer 1, alias to semantic tokens at layer 2.
  5. Search-replace raw hex strings in component CSS with the semantic token.

The entire migration took about 28 minutes for 12 tokens. The resulting code is measurably easier to read: you can infer from oklch(0.30 0.20 264) that this is a dark, saturated blue without a color preview.


Made by Toolora · Updated 2026-06-30