Home / Articles / Color & Theming /
OKLCH from scratch: lightness, chroma, hue
The oklch() color function gives you perceptually uniform colors in CSS. Here is everything you need to know about its three channels.
Why OKLCH exists
Traditional color models like RGB and HSL operate in the sRGB color space, which was designed for CRT monitors, not human perception. In HSL, two colors with identical saturation and lightness values can look wildly different in brightness depending on hue. A yellow at hsl(60 100% 50%) appears far brighter than a blue at hsl(240 100% 50%), despite sharing the same L value.
OKLCH solves this by mapping lightness to how the human visual system actually perceives brightness. Equal lightness values produce equal perceived brightness, regardless of hue. This is what "perceptually uniform" means.
/* Same lightness (0.7), same chroma (0.15) — different hues */
.yellow { color: oklch(0.7 0.15 90); }
.blue { color: oklch(0.7 0.15 260); }
.red { color: oklch(0.7 0.15 25); }
/* All three appear equally bright to the human eye */
The three channels explained
The oklch() function takes three required values and one optional alpha:
/* oklch(L C H / alpha) */
/* L — Lightness: 0 (black) to 1 (white)
How bright the color appears.
0.5 is a true perceptual mid-tone. */
/* C — Chroma: 0 (gray) to ~0.4 (max saturation)
How colorful vs. gray. Unlike HSL saturation,
the maximum chroma varies by hue. */
/* H — Hue: 0–360 degrees
The color angle on the hue wheel.
0 = pink-red, 90 = yellow, 145 = green,
260 = blue, 330 = magenta. */
.example {
background: oklch(0.65 0.2 260); /* vivid blue */
color: oklch(0.95 0.03 260); /* very light blue-gray */
border-color: oklch(0.65 0.2 260 / 0.3); /* same blue, 30% opacity */
}
The alpha channel works like any other CSS color function — a value from 0 (fully transparent) to 1 (fully opaque), separated by a slash.
Lightness: the most useful channel
Lightness is the star of OKLCH. Because it maps to human perception, you can create tint and shade scales with predictable visual steps. A step of 0.1 in lightness always looks like the same amount of change, whether you are working with reds, blues, or greens.
/* A 5-step lightness scale for a brand blue (hue 265) */
:root {
--brand-100: oklch(0.92 0.06 265); /* lightest tint */
--brand-300: oklch(0.76 0.13 265); /* light */
--brand-500: oklch(0.60 0.20 265); /* base */
--brand-700: oklch(0.44 0.18 265); /* dark */
--brand-900: oklch(0.28 0.12 265); /* darkest shade */
}
/* Each step is ~0.16 apart in L, producing even visual steps */
Keeping chroma consistent (or reducing it slightly at the extremes) ensures the scale feels cohesive. At very low lightness, maximum chroma decreases naturally, so you lower C for the darkest shades.
Chroma: controlling saturation correctly
Chroma determines how vivid or muted a color looks. Unlike HSL saturation (0–100%), chroma has no fixed upper bound. The maximum depends on the hue and lightness — some colors (like cyan at mid-lightness) can reach chroma values above 0.3, while others peak lower.
/* Chroma from gray to vivid */
.neutral { color: oklch(0.6 0 0); } /* pure gray, no hue matters */
.muted { color: oklch(0.6 0.05 260); } /* barely tinted */
.medium { color: oklch(0.6 0.12 260); } /* moderate blue */
.vivid { color: oklch(0.6 0.22 260); } /* saturated blue */
.max { color: oklch(0.6 0.30 260); } /* near maximum for this hue */
/* Practical tip: 0.15–0.22 is the sweet spot for UI accent colors.
Below 0.08 reads as "tinted gray". Above 0.25 can feel electric. */
When chroma is 0, the hue value is irrelevant — you get a neutral gray determined entirely by lightness. This makes it easy to mix grays and colors in the same system.
Hue: navigating the color wheel
Hue in OKLCH uses degree angles just like HSL, but the colors at each angle differ slightly due to the perceptual mapping. Key hue landmarks:
/* OKLCH hue reference */
--red: oklch(0.6 0.22 25); /* warm red */
--orange: oklch(0.7 0.18 55); /* orange */
--yellow: oklch(0.85 0.17 90); /* yellow (needs higher L) */
--green: oklch(0.6 0.2 145); /* green */
--teal: oklch(0.6 0.14 195); /* teal */
--blue: oklch(0.6 0.2 260); /* blue */
--purple: oklch(0.5 0.22 300); /* purple */
--magenta: oklch(0.6 0.22 340); /* magenta/pink */
/* Complementary pairs sit 180° apart:
Blue (260) ↔ Yellow-orange (80)
Red (25) ↔ Teal (205) */
A useful technique: pick your brand hue, then derive accents by rotating 30°, 60°, or 120° while keeping lightness and chroma fixed. The result is a harmonious palette with consistent brightness.
Gamut clamping and the browser
Not every OKLCH value maps to a displayable color on every screen. When you specify a color outside the sRGB gamut (for example, very high chroma at certain hues), the browser automatically clamps it to the nearest displayable color. This is called gamut mapping.
/* This chroma is beyond sRGB gamut for this hue/lightness.
The browser will clamp to the nearest displayable color. */
.too-vivid {
background: oklch(0.7 0.35 145);
}
/* Safe approach: stay within sRGB limits.
Use chroma ≤ 0.2 at mid lightness for broad compatibility. */
.safe {
background: oklch(0.7 0.18 145);
}
/* For wide-gamut displays (P3), you can push higher: */
@media (color-gamut: p3) {
.enhanced {
background: oklch(0.7 0.28 145);
}
}
In practice, clamping is invisible — the browser picks the closest color and nothing breaks. But for maximum control, stay within sRGB-safe chroma values (roughly 0.2 or below at mid lightness) unless you intentionally target wide-gamut screens.
Putting it all together
Here is a complete example combining lightness, chroma, and hue knowledge to build a dark-mode card component:
.card {
/* Background: low lightness, minimal chroma for subtle tint */
background: oklch(0.19 0.02 260);
/* Border: same hue, slightly lighter, low chroma */
border: 1px solid oklch(0.3 0.04 260);
/* Text: high lightness, near-zero chroma */
color: oklch(0.93 0.01 260);
border-radius: 0.75rem;
padding: 1.5rem;
}
.card__title {
/* Accent: mid-high lightness, moderate chroma */
color: oklch(0.72 0.19 265);
font-weight: 700;
}
.card__meta {
/* Muted: mid lightness, very low chroma */
color: oklch(0.63 0.02 260);
font-size: 0.875rem;
}
Every color shares the same hue family (260–265) with intentional variation only in lightness and chroma. This produces a cohesive, professional result that scales to any number of UI states.