OKLCH vs HSL vs RGB: a simple CSS guide
Hex and RGB tell a screen how much red, green, and blue to turn on. #0000FF is no red, no green, and full blue. HSL writes that same color as a hue, a saturation, and a lightness: hsl(240 100% 50%). “Lightness” sounds like how bright the color looks. In HSL, it is not. It is only a position in that formula.
OKLCH is a way to write a CSS color so the lightness matches what you see. Björn Ottosson published the space behind it, OKLab, in 2020. OKLCH is that space drawn as a wheel: how light, how vivid, and which hue.
The three numbers
In oklch(L C H) there are three numbers. OK is the name of the space, not an extra number.
L 0.70 Lightness. How light it looks, from 0 (black) to 1 (white).
C 0.12 Chroma. How vivid it is. Zero would be gray.
H 240° Hue. Where it sits on the wheel. 240° is this blue.
oklch(0.70 0.12 240) is the blue above. The pictures on this page use colors you can check by eye. Every number was measured with chroma.js 3.2.0. You can mix a pair the same way in the sandbox.
Why HSL lightness does not match what you see
HSL can give six colors the same lightness number and still make yellow glare while blue looks dark.
Six colors, all marked 50%
Each chip is hsl(H 100% 50%). The bar under it is OKLCH lightness: how light the color looks.
Yellow’s bar is almost full. Blue’s stops short of halfway. HSL called both 50%.
Try it
Drag the hue. The left chip keeps HSL lightness at 50%. The right chip keeps OKLCH lightness at 0.70.
.red { background: hsl(0 100% 50%); } /* #FF0000 */
.yellow { background: hsl(60 100% 50%); } /* #FFFF00 */
.green { background: hsl(120 100% 50%); } /* #00FF00 */
.cyan { background: hsl(180 100% 50%); } /* #00FFFF */
.blue { background: hsl(240 100% 50%); } /* #0000FF */
.magenta { background: hsl(300 100% 50%); } /* #FF00FF */
The same colors, with text on them
A palette is also type, not only chips. White and black text on that yellow and that blue:
White type on the yellow nearly disappears. On the blue, it is easy to read. Black type flips the result.
For normal text, WCAG 2.2 asks for 4.5:1 (AA) and 7:1 (AAA). White on this yellow is 1.07:1, so it fails. White on this blue is 8.59:1, so it passes AAA. Change only the hue in HSL and a color that passed can fail, without you touching the lightness number.
The contrast badge on the sandbox measures this same ratio for the theme you are looking at. Andrey Sitnik and Travis Turner show the same failure in palette tools, including the old Sass darken().
The measurements behind the row
“How light it looks” is OKLCH L. “Screen brightness” is relative luminance, the Y that WCAG uses for contrast. They are related, and they are not the same number.
| Color | How light it looks | Screen brightness | White text | Black text |
|---|---|---|---|---|
| #FF0000 | 0.628 | 0.2126 | 4.00:1 | 5.25:1 |
| #FFFF00 | 0.968 | 0.9278 | 1.07:1 | 19.56:1 |
| #00FF00 | 0.866 | 0.7152 | 1.37:1 | 15.30:1 |
| #00FFFF | 0.905 | 0.7874 | 1.25:1 | 16.75:1 |
| #0000FF | 0.452 | 0.0722 | 8.59:1 | 2.44:1 |
| #FF00FF | 0.702 | 0.2848 | 3.14:1 | 6.70:1 |
Yellow’s screen brightness is 0.9278. Blue’s is 0.0722. The yellow is about 12.9 times as bright, while HSL gave both lightness 50%. In OKLCH the same pair is 0.968 and 0.452.
What changes when lightness stays put
Set the lightness once, then turn the hue. The family should stay one brightness.
One lightness, six hues
Each chip is oklch(0.70 0.12 H). Lightness and chroma stay fixed. Only the hue changes. All six fit on a normal sRGB screen.
The bars are the same length. Changing the hue no longer throws the brightness from yellow-bright to blue-dark.
.tone { background: oklch(0.70 0.12 var(--hue)); }
.tone-0 { --hue: 0; } /* #DA7D9B */
.tone-60 { --hue: 60; } /* #D48B4B */
.tone-120 { --hue: 120; } /* #95A94E */
.tone-180 { --hue: 180; } /* #24B6A1 */
.tone-240 { --hue: 240; } /* #4FA8E1 */
.tone-300 { --hue: 300; } /* #AA8DDE */
White text on this row stays between 2.53:1 and 2.83:1, so it fails AA on every hue. Black text stays between 7.41:1 and 8.31:1, so it clears AAA on every hue. You still check contrast. What you gain is that the check no longer swings wildly when you recolor.
WCAG computes its ratio from screen brightness, not from OKLCH L. On this row the two travel together. They are still not the same quantity. Alperen Bekçi treats that stable lightness as the reason OKLCH is easier to keep readable than hex or RGB. The swatches support the stability. They do not replace the ratio.
| Color | Hue | Screen brightness | White text | Black text |
|---|---|---|---|---|
| #DA7D9B | 0° | 0.3207 | 2.83:1 | 7.41:1 |
| #D48B4B | 60° | 0.3312 | 2.75:1 | 7.62:1 |
| #95A94E | 120° | 0.3538 | 2.60:1 | 8.08:1 |
| #24B6A1 | 180° | 0.3656 | 2.53:1 | 8.31:1 |
| #4FA8E1 | 240° | 0.3494 | 2.63:1 | 7.99:1 |
| #AA8DDE | 300° | 0.3272 | 2.78:1 | 7.54:1 |
Screen brightness now spans 0.3207 to 0.3656, about 1.14 times. In the HSL row above, yellow versus blue was about 12.9 times.
Why a blue-to-yellow gradient turns gray
#0000FF is OKLCH lightness 0.452, chroma 0.313, hue 264.1°. #FFFF00 is lightness 0.968, chroma 0.211, hue 109.8°. Blend them three ways:
Try it
Drag along the same blue-to-yellow blend. Each chip is the color painted on that bar.
.mix-srgb { background: linear-gradient(to right in srgb, #0000ff, #ffff00); }
.mix-hsl { background: linear-gradient(to right in hsl, #0000ff, #ffff00); }
.mix-oklch { background: linear-gradient(to right in oklch, #0000ff, #ffff00); }
What the middle of each bar is
sRGB averages the channels. Halfway is 127.5, 127.5, 127.5: gray #808080, with no chroma left. Gabriel Shoyombo describes this gray stretch.
HSL keeps its own lightness at 50% and, because the hue gap is 180°, walks through green. Halfway is #00FF80. It looks much lighter than the blue: OKLCH lightness 0.875, close to the yellow. HSL held its number and the color still jumped in brightness.
OKLCH moves lightness from 0.452 to 0.968 and keeps the color strong. The exact middle, before the browser fits it onto the screen, is oklch(0.710 0.262 186.9). That chroma does not fit in sRGB. The browser maps it. The last section shows what that mapping is, and what it is not.
How to lighten or recolor without guesswork
Start with one blue, #488ACB, written oklch(0.62 0.12 250).
The middle chip only raises lightness, from 0.62 to 0.70. The right chip only changes the hue, from 250° to 25°. Hex cannot tell you which of those you did.
Try it
Drag one number at a time, or use the buttons. Lightness, chroma, and hue move on their own.
White text on the three is 3.63:1, 2.66:1, and 3.85:1. Changing the hue moved the ratio by 0.22. In the HSL row, blue to yellow moved it by 7.52.
CSS can name the edit in place. The hover adds 0.08 to lightness. The delete state keeps lightness and chroma, and sets the hue to 25.
:root {
--accent: oklch(0.62 0.12 250); /* #488ACB */
}
.button {
background: var(--accent);
}
.button:hover {
background: oklch(from var(--accent) calc(l + 0.08) c h); /* #61A3E6 */
}
.button.is-delete {
background: oklch(from var(--accent) l c 25); /* #C46761 */
}
The export panel writes this kind of token as CSS variables and leaves the markup alone.
Colors past ordinary sRGB
rgb() and hex only name sRGB colors. Many newer screens can show a wider set, Display P3. oklch() can point outside sRGB, and CSS Color 4 defines that wider space. Sitnik and Turner cite an estimate that sRGB covers about 35% of the colors people can see, and that P3 adds about 30% more. Those figures describe how large the ranges are. They were not measured for the tables on this page. On an sRGB screen, a color that does not fit is mapped to one that does.
A closer look at hue
There is an older “lightness, chroma, hue” space, CIE LCh. It comes from CIE 1976 L*a*b*, written up in CIE 015:2018. A hue that stays fixed there does not stay fixed in the blues. That is why Ottosson made OKLab, and why CSS uses oklch() for the newer wheel. OKLab itself is not a CIE standard.
The older wheel lets blue slide
These three share CIE LCh lightness 50 and hue 290. Only the chroma changes, from strong to pale. Watch the hue number under each chip. It does not stay put. None of the three was clipped.
You asked for one hue and got three. The blue drifts as it gets weaker.
.drift-80 { background: lch(50 80 290); } /* #126FF9, OKLCH hue 259.8 */
.drift-40 { background: lch(50 40 290); } /* #6772B7, OKLCH hue 275.1 */
.drift-20 { background: lch(50 20 290); } /* #727597, OKLCH hue 281.1 */
The same fade in OKLCH stays put
At lightness 0.55 and hue 274, lowering the chroma keeps the hue at 274°.
Strong, medium, pale. The hue under each chip is the same.
.hold-25 { background: oklch(0.55 0.25 274); } /* #4F51FF */
.hold-12 { background: oklch(0.55 0.12 274); } /* #5C6AB7 */
.hold-06 { background: oklch(0.55 0.06 274); } /* #666F95 */
What OKLCH still gets wrong
OKLCH is the useful form when you are building or adjusting a palette. It does not remove the two checks that come after: does this color fit on the screen, and does the text pass contrast?
A written color may not fit the screen
A legal oklch() value is not always a color an ordinary screen can show. At lightness 0.70, chroma 0.12 fit for every hue in the row above. Chroma 0.13 was already too much for the cyan, in chroma.js. The limit depends on the hue, so one chroma number cannot be copied around the whole wheel.
The vivid middle of a gradient can fall outside
Both ends of the blue-to-yellow gradient fit on screen. The OKLCH middle still steps outside, because hue 186.9° has a narrower range. oklch(0.710 0.262 186.9) does not fit in sRGB. The strongest chroma that does fit, at that lightness and hue, is 0.124, which is #02BAAE.
CSS pulls an out-of-range color in by lowering chroma and keeping lightness and hue. That painted color is this kind of reduction. It is not the channel clip chroma.js produced earlier, #00CFBD, and it is not the chroma you wrote in the stylesheet.
.asked { background: oklch(0.710 0.262 186.9); } /* outside sRGB */
.fits { background: oklch(0.710 0.124 186.9); } /* #02BAAE, the strongest that fits */
They are close, and they are not the same edit. The left chip is what you get by chopping channels. The right chip is the strongest color that still has the lightness and hue you asked for.
Mixing the same pair in OKLab stays inside sRGB. The middle is #6CABC7: lightness 0.710, chroma 0.077, hue 227.4°. in oklch moves lightness, chroma, and hue on their own. in oklab stays on the straight line between the two colors.
.along-oklch { background: linear-gradient(to right in oklch, #0000ff, #ffff00); }
.along-oklab { background: linear-gradient(to right in oklab, #0000ff, #ffff00); }
Matching lightness is not a contrast pass
At OKLCH lightness 0.70, white-text contrast only ranged from 2.53:1 to 2.83:1. That is stable, and none of it is a WCAG pass until you measure it. The sandbox badge does that measurement for the live theme.
Hex and RGB remain what a file can store. HSL remains a view of those same colors. After the edit, check that the color fits, and check the contrast.
Sources
Measurements on this page use chroma.js 3.2.0. OKLCH components are that library’s conversion. Screen brightness and the contrast ratios follow the WCAG 2 definition. A color is listed as fitting on screen only when chroma.js did not clip it.
Standards:
- W3C. CSS Color Module Level 4. Defines
oklch(), OKLab,display-p3, and the method that maps an out-of-range color in OKLCH. - W3C. CSS Color Module Level 5. Defines relative color syntax, including
oklch(from …). - CIE. Colorimetry, 4th edition, CIE 015:2018. Defines CIE 1976 L*a*b*, the space behind CIELCh. OKLab is not a CIE standard.
- W3C. Web Content Accessibility Guidelines (WCAG) 2.2, success criterion 1.4.3 Contrast (Minimum), and the contrast ratio and relative luminance definitions. The ratio uses sRGB luminance.
- IEC 61966-2-1:1999. Multimedia systems and equipment — Colour measurement and management — Part 2-1: Colour management — Default RGB colour space — sRGB.
The OKLab model, and the blue hue error it was built to reduce:
- Björn Ottosson. “A perceptual color space for image processing.” 23 December 2020.
Articles this note drew on:
- Andrey Sitnik and Travis Turner. “OKLCH in CSS: why we moved from RGB and HSL.” Evil Martians, 17 September 2025. The 35% sRGB and 30% P3 estimates are cited from this article.
- Gabriel Shoyombo. “oklch().” CSS-Tricks, 3 April 2025.
- Alperen Bekçi. “Why OKLCH is a Superior Choice Over HEX and RGB.” Medium, 18 May 2025.