OKLCH Color Codes Explained: Perceptually Uniform Color in CSS
Color Code5 min read
If you have ever darkened a button by dropping HSL lightness by 10% and watched the hue shift or the color turn muddy, you have hit the limit of RGB and HSL: they were designed for hardware, not for eyes. OKLCH is the color model browsers now ship that fixes this — the same lightness means the same perceived brightness, in every hue.
What the three values mean
OKLCH is the cylindrical form of OKLab, a perceptual color space published by Björn Ottosson in 2020. It has three components plus an optional alpha channel:
- L — lightness, written as a number from
0to1or a percentage from0%to100%.0is black,1is white, and — unlikehsl()— this is perceived lightness. - C — chroma, how colorful the color is.
0is gray; values climb from there, and in CSS100%is equal to0.4. sRGB colors rarely exceed about0.32. - H — hue, an angle in degrees. OKLCH hue angles do not line up with HSL's: red sits near 29°, yellow near 110°, green near 142° and blue near 264°.
Here are real sRGB colors written in both models:
| Color | Hex | OKLCH |
|---|---|---|
| White | #FFFFFF | oklch(100% 0 0) |
| Yellow | #FFFF00 | oklch(96.8% 0.211 109.8) |
| Green | #00FF00 | oklch(86.6% 0.295 142.5) |
| Mid gray | #808080 | oklch(60% 0 0) |
| Red | #FF0000 | oklch(62.8% 0.258 29.2) |
| Blue | #0000FF | oklch(45.2% 0.313 264.1) |
| Black | #000000 | oklch(0% 0 0) |
Sort the table by that first number and you get a brightness order that matches what your eyes report: yellow is nearly as light as white, blue is less than half as light.
Why OKLCH beats HSL for lightness
Take the four most saturated colors in sRGB. In HSL they are written identically except for hue — hsl(…, 100%, 50%) — which implies they are equally light:
| Color | Hex | HSL | OKLCH lightness |
|---|---|---|---|
| Yellow | #FFFF00 | hsl(60 100% 50%) | 96.8% |
| Green | #00FF00 | hsl(120 100% 50%) | 86.6% |
| Red | #FF0000 | hsl(0 100% 50%) | 62.8% |
| Blue | #0000FF | hsl(240 100% 50%) | 45.2% |
In HSL those colors share one lightness value; in OKLCH their perceived lightness spans 45% to 97%. That single difference is why an HSL-generated ramp looks uneven (yellow steps look tiny, blue steps look huge) while an OKLCH one looks regular. If you are coming from HSL, think of OKLCH as the same three-dial idea with a lightness dial you can finally trust.
The CSS syntax
The modern syntax is space-separated, with alpha after a slash:
:root {
--brand: oklch(54.1% 0.247 293); /* a vivid violet (#7C3AED) */
--brand-soft: oklch(94% 0.03 293); /* same hue, low chroma */
--brand-glass: oklch(54.1% 0.247 293 / 35%);
}Percentage lightness is allowed (54.1% equals 0.541), and none is valid for a component you want the browser to fill in during interpolation. Relative color syntax lets you derive values from an existing color without repeating numbers:
.button {
background: var(--brand);
border-color: oklch(from var(--brand) calc(l - 0.12) c h);
color: oklch(from var(--brand) 0.98 0.02 h);
}Read that as "same chroma and hue as --brand, but 12 points darker" and "same hue, almost white, nearly gray". In HSL a similar trick exists, but in OKLCH the result really is darker by the same perceived amount.
Wide gamut: where OKLCH goes past sRGB
Every hex code lives inside sRGB, the color space defined for 1990s CRT monitors. Modern phones, laptops and monitors cover Display P3, a wider gamut with noticeably deeper reds, oranges and greens.
OKLCH is not limited to sRGB, which is why CSS added both oklch() and the color() function with a color-space argument:
.vivid {
/* P3 can render a redder red than #FF0000 */
background: color(display-p3 1 0 0);
}
@media (color-gamut: p3) {
.vivid { background: oklch(62.8% 0.29 29.2); }
}On a wide-gamut screen those colors render more saturated; on an sRGB-only display the browser maps them into the smaller gamut, so they appear as the closest reproducible color instead of breaking. The color-gamut media query lets you opt into the extra saturation only where it is supported.
Building ramps and gradients
Two habits make OKLCH useful in a design system:
- Hold chroma and hue, step lightness.
oklch(95% 0.03 293)→90%→80%→ … →20%gives evenly spaced steps that read as one color getting darker. The shades generator builds comparable tint and shade ramps for any color. - Interpolate in OKLCH. Gradients normally blend in sRGB, which produces dull midpoints between distant hues. Modern browsers accept an explicit interpolation space:
.hero {
background: linear-gradient(in oklch, #7C3AED, #0891B2);
}That is not a curiosity — Tailwind CSS v4 defines its entire default palette in OKLCH for exactly this reason, so a blue-500 lightened or mixed in a modern browser behaves more predictably than the old sRGB hex ramp.
Browser support and safe fallbacks
oklch() has been widely available since mid-2023: Safari shipped the CSS Color 4 spaces first in 15.4, with Chrome 111 and Firefox 113 following in 2023. Older browsers ignore a declaration they cannot parse, so the reliable pattern is a fallback first, then the upgrade:
.badge {
background: #7c3aed; /* sRGB fallback */
background: oklch(54.1% 0.247 293); /* used when supported */
}For conditional logic, @supports (color: oklch(0% 0 0)) works as expected. One caution: perceptual lightness is not contrast. Two colors with very different OKLCH lightness will usually pass WCAG, but the actual ratio depends on the pair, so check the finished combination with the contrast checker.
When to use OKLCH
- Use it for design tokens, generated ramps, theme colors, gradients, and anything you plan to lighten or darken programmatically.
- Keep hex and RGB where a value must be exact and portable: brand hand-offs, canvas and image APIs, and any platform that has not adopted CSS Color 4.
- Keep HSL when you are teaching, reasoning simply, or need maximum compatibility — it is not wrong, just less precise about lightness.
Try it: pasteoklch(96.8% 0.211 109.8)andoklch(45.2% 0.313 264.1)into the color picker. They are the yellow and blue from the table above, and comparing their lightness readouts is the fastest way to see why OKLCH exists.
OKLCH is what happens when a color model is designed around perception instead of phosphors: lightness you can predict, clean math for ramps, and access to the wider gamut modern screens already have.
Tools for this guide
Related posts
HSL Color Codes Explained: Hue, Saturation and Lightness
HSL color codes describe a color the way a designer thinks about it — which hue it is, how saturated it is and how light it is. Here is how to read hsl(), how it differs from RGB, and when HSL beats hex.
Colors With Opacity in CSS: RGBA, HSLA and 8-Digit Hex
You can add transparency to any CSS color with rgba(), hsla() or 8-digit hex. Here is how each syntax works, when to use which, and the pitfalls to avoid.