OKLCH describes a colour by three intuitive numbers — perceptual lightness (L), chroma (C), and hue angle (H) — in a space engineered so that equal changes in the numbers produce roughly equal changes in what your eye sees. That perceptual uniformity is the whole point: it fixes the long-standing problem that HSL’s “lightness” does not match perceived brightness, and it makes tasks like building an accessible palette or generating even tint ramps predictable instead of trial-and-error. The syntax landed in modern CSS as oklch(), and it is worth understanding what each component controls.
What do the three OKLCH values mean?
L is lightness from 0 to 1 (or 0% to 100%), C is chroma starting at 0 with no fixed maximum, and H is a hue angle from 0 to 360 degrees. The CSS function takes them in that order:
color: oklch(0.7 0.15 250); /* L=0.7 C=0.15 H=250deg (a blue) */
color: oklch(70% 0.15 250); /* same, L written as a percentage */
color: oklch(0.7 0.15 250 / 0.5); /* with 50% alpha */
Lightness answers “how bright.” Zero is black, the maximum is white, and the value is designed to line up with human perception of brightness. Chroma answers “how colourful,” from a neutral grey at 0 outward to increasingly vivid colour. Hue is the familiar colour wheel angle — roughly reds near 0 and 360, greens near 140, blues near 250 — the same mental model you already have from HSL, just on a better-behaved wheel.
Why is HSL lightness misleading?
In HSL, two colours with identical lightness values can look dramatically different in brightness, because HSL lightness is a simple mathematical midpoint rather than a perceptual measurement. A pure yellow and a pure blue at hsl(H 100% 50%) share the same 50% lightness, yet the yellow looks far brighter to the eye than the blue. That mismatch makes HSL unreliable for anything that depends on consistent perceived brightness — which is most of visual design.
The practical consequences show up constantly. If you build a set of category colours all at the same HSL lightness expecting them to feel balanced, some will jump out and others will recede. If you try to guarantee text contrast by fixing lightness, the guarantee does not hold across hues. OKLCH was built specifically so that a given L corresponds to a consistent perceived brightness regardless of hue, which is what lets you reason about colour numerically.
| Property | HSL | OKLCH |
|---|---|---|
| Lightness meaning | Mathematical midpoint | Perceived brightness |
| Equal L looks equally light? | No | Approximately yes |
| Saturation upper bound | Fixed at 100% | Chroma has no fixed max |
| Wide-gamut colours | sRGB only | Can exceed sRGB |
How does perceptual uniformity help build palettes?
Because equal steps in L produce equal steps in perceived lightness, you can generate a tint-and-shade ramp by simply walking the L value while holding hue and chroma steady. This is the task that HEX and HSL make frustrating: pick a brand blue, then produce five lighter and five darker versions that feel evenly spaced. In HSL the steps bunch up or spread out unpredictably. In OKLCH you choose your hue and a chroma, then set lightness at even intervals:
--blue-100: oklch(0.95 0.03 250);
--blue-300: oklch(0.85 0.07 250);
--blue-500: oklch(0.70 0.15 250);
--blue-700: oklch(0.55 0.13 250);
--blue-900: oklch(0.35 0.09 250);
Each row shares the hue, so the family reads as one colour; the lightness marches down in even perceptual steps; and chroma tapers near the light and dark ends because very light or very dark colours cannot hold as much chroma. The result is a ramp that looks deliberately designed rather than hand-tuned. Keeping hue constant across a ramp also means hovering, active, and disabled states can be derived by nudging a single number. To see how a familiar HEX or HSL value translates into these three components, a client-side colour converter lets you paste a value and read its equivalents without any of it leaving your browser.
What is chroma and why has it no fixed maximum?
Chroma is the amount of colourfulness, and unlike HSL saturation it is not normalised to a 0–100 scale, because the maximum achievable chroma depends on the lightness, the hue, and the display’s gamut. There is simply no single “most saturated” value that applies everywhere. A mid-lightness colour can sustain a high chroma; the same hue pushed very light or very dark cannot, because the display runs out of room to make it that colourful at that brightness.
In practice you work with chroma values that are often small numbers — something in the range of roughly 0.1 to 0.2 covers most vivid sRGB colours, while neutrals sit near 0. If you request a chroma higher than the display can render for a given L and H, the colour falls outside the gamut and the browser maps it back in, which may clip it to something slightly less saturated. This is not a bug; it is the model honestly telling you that the colour you asked for is more vivid than the screen can show.
Can OKLCH reach colours sRGB cannot?
Yes — OKLCH is not tied to sRGB, so it can express colours in wider gamuts such as Display P3, which modern screens increasingly support. A high-chroma OKLCH value may sit outside the sRGB triangle entirely. On a wide-gamut display the browser can render it as the more vivid colour it describes; on a narrower display it maps the value into the available gamut. This makes OKLCH forward-looking: the same declaration can look richer on capable hardware while degrading gracefully elsewhere.
It also means you should not assume every OKLCH value is displayable as written. Treat the model as describing an intent, and let gamut mapping resolve it per device. For colours you know must stay within sRGB — for instance to match an exact brand HEX — keep the chroma modest, or convert from a known sRGB value so you start inside the gamut.
When should I use OKLCH in a stylesheet?
Reach for OKLCH whenever you are reasoning about colour relationships numerically — accessible contrast, evenly spaced scales, consistent state variants — and keep HEX or named colours for one-off fixed values where you already know the exact result you want. The strengths line up neatly with real design tasks: build a type-and-surface palette where equal lightness reads as equally light, derive tints by stepping L, and shift a whole theme’s temperature by nudging H across your tokens.
OKLCH also pairs naturally with CSS custom properties and the relative colour syntax that lets you derive one colour from another. Because each component is an independent number, you can store a base colour and produce variants by adjusting a single channel — dropping lightness for a pressed state, trimming chroma for a disabled look, or rotating hue to generate an accent — all while keeping the family coherent. That is far harder with HEX, where every derived shade is an opaque new code with no visible relationship to the original. Storing colour as three meaningful numbers turns a palette from a list of magic strings into a small system you can reason about and adjust in one place.
A comfortable workflow is to define your palette tokens in OKLCH so the relationships are explicit and editable, then let the browser handle conversion to whatever the display needs. Because L maps to perceived brightness, you can also make sensible contrast decisions by looking at the difference in L between text and background, rather than guessing. OKLCH does not replace understanding contrast requirements, but it gives you a colour space where the numbers finally mean what your eyes expect — which is precisely what HEX, RGB and HSL never quite delivered.