It's Time to Learn oklch Color
If you’re anything like me, looking at all the things happening in CSS lately involving color, you’re probably a bit overwhelmed. For a long time, HSL was promoted as the human readable alternative to hex or RGB. For about a decade, HSL has been the best option for working comfortably with color on the web.
But now, somewhat suddenly, we’ve got several new options thrown into the mix:
hwb()
, lab()
, lch()
, oklab()
, and oklch()
.
There’s also color()
which sort of fits into the same category and kind of doesn’t.
This is a lot to take in.
But hsl()
is great!
So why leave what works and what’s comfortable?
It’s easy to get lost in all this. So I’m going to try to make it easy: If you don’t know where to start, or which of these things is going to be practically useful enough to be worth your time, I think the biggest bang for your buck is to learn OKLCH (or, “Oklachroma”, if I had my way and could make that catch on). Hopefully I can convince you that it’s worth diving in and learning.
Why OKLCH¶
I’m not going to take a deep dive into all the various color notations in this post. But HSL has been king for over a decade now, so I will draw upon that to make some important comparisons.
The thing that makes HSL so great is that it is so much more intuitive
than hex or RGB color.
One value for Hue, or color of the rainbow.
One value for Saturation, or how vivid that color is.
And one value for Lightness, ranging from black to white.
For example, hsl(220deg 60% 45%)
— a fairly vivid, medium-dark blue.
OKLCH follows a very conceptually similar pattern. The mental model is nearly identical, though the order of the values is reversed:
- Lightness: from 0% to 100%
- Chroma: vividness of color, from 0.0 to 0.37 (more on that below)
- Hue: from 0deg to 360deg indicating the color of the rainbow
There are two key distinctions from HSL, however.
It’s based on human perception
In HSL, lightness and saturation each range from 0% to 100%, where 0% means none and 100% means the highest amount of light or saturated color that can be represented in the sRGB gamut— sRGB is the range of colors that have historically been available to most color monitors.
The problem with this is it doesn't quite align with the way our eyes perceive light. Look at these two HSL colors, each of which has an equal HSL lightness value of 50%:
hsl(217deg 55% 50%)
hsl(110deg 55% 50%)
Both colors have the same specified lightness, but the second one appears noticeable brighter to our eyes. Here are the same colors in OKLCH:
oklch(55% 0.15 260)
oklch(73% 0.2 141)
Notice how in OLKCH, the lightness is different (55% and 73%). Look what happens when we bring the lightness of the green down to match the blue:
oklch(55% 0.15 260deg)
oklch(55% 0.2 141deg)
By giving them the same lightness value in OKLCH, the green has been dimmed. Now they both appear equally bright. You'll also noticed that the specified chroma value is different between the two colors (0.15 and 0.2). Let's adjust the chroma values to match:
oklch(55% 0.15 260deg)
oklch(55% 0.15 141deg)
The difference here is a little more subtle, but the green on the right has been desaturated from the previous example. Now the two colors both appear to have the same brightness and the same vividness.
In HSL, 100% saturation is simply as saturated as that particular color can be in the sRGB gamut. In OKLCH, the values aren't based on technical limits or a mathematical definition, but rather on perceived equality. The amount if lightness indicates exactly how bright the color is, and the amount of chroma indicates exactly how vivid it is. The human eye perceives some colors like green or yellow to be brighter than others, like blue or purple, and OKLCH takes these details into account.
It can define any color
I’ve mentioned a couple times that HSL is limited to the sRGB gamut. But monitors are getting better. Most smartphones and newer monitors, including most Mac laptops, support a wider range of colors (a gamut called P3). And some high-end monitors offer an even wider range of colors (a gamut called Rec2020).
The great thing about OKLCH, is it can specify any color these monitors are capable of—In fact, it can specify any color that the human eye is capable of seeing. In CSS, the browser will automatically round any out-of-range colors to the nearest color the hardware is capable of displaying.
Browser support is nearly there
At the moment, OKLCH is supported in Chrome, Edge, and Safari. Firefox supports it only behind a flag at the moment, but it is expected to be enabled by default soon, with version 113. See the latest browser support at caniuse.com.
That means it’s probably not ready to completely replace your HSL yet, but that day is near. In the meantime, you can use it selectively with feature queries. I’m doing a little of that on this site right now, where I wanted to push some of the color accents into a slightly more vivid range for browsers and monitors that support it:
@supports (color: oklch(73% 0.17 192)) {
:root {
--accent-color-1: oklch(73.54% 0.169 193);
--accent-color-2: oklch(68.15% 0.272 9);
--accent-color-3: oklch(77.94% 0.203 62);
--accent-color-4: oklch(66.67% 0.193 253);
}
}
Or you can just use regular old fallback values:
background-color: hsl(179 100% 38%);
background-color: oklch(73% 0.17 193);
Just be aware that this latter approach does not work for
custom properties, because the value won’t resolve as invalid
until you reference it later using var()
.
How to use OKLCH¶
So hopefully I’ve convinced you to give OKLCH a shot, even if it’s just with some simple experimentation to get comfortable with it.
The first thing you need to know is that it uses the “new” style of CSS
color syntax.
And by that, I mean there are no commas between the values:
oklch(50% 0.3 280deg)
.
If you want to add an alpha channel for transparency, use a slash to
denote it: oklch(50% 0.3 280deg / 0.5)
.
The older color functions have all been updated to use this approach as well.
So instead of the old hsl(200deg, 50%, 45%)
, you can now use a comma-free
notation: hsl(200deg 50% 45%)
.
And instead of a separate function for transparency (hsla()
), you can
use the same function with a slash: hsl(200deg 50% 45% / 0.5)
.
For hsl()
and rgb()
the old notation with commas will be supported
for backwards-compatibility, but going forward the new color functions
will not.
Chroma
There are a few things to be aware of when using oklch()
.
The most important one is the chroma range.
Unlike saturation in HSL, chroma is not a percentage.
For all intents and purposes, the chroma value is a number between 0 and 0.37. You can try using a higher chroma value, like 25, but it is going to round to a color in the monitor’s supported range and the result can be unpredictable (you may specify a blue hue, but it might end up selecting a teal if it’s vivid enough to be “closer” to the values you specified).
Theoretically, OKLCH color can specify colors with chroma up to infinity, but I find this nonsensical. I have a hard time imagining a red much more vivid than this:
oklch(50% 0.37 29deg)
Maybe I would believe some hyper-intense paint could push it to something like 0.5, but that’s about it. So I don’t find the “infinity” limit helpful in the slightest. Keep it under 0.37.
In fact, for most hues, the practical limit is even lower. This orange, for example, is maxed out at 0.187:
oklch(70% 0.187 60)
If you were to increase the lightness, you could bump up the chroma a smidge further. But the point is, the values are all interelated. If you have a very dark color, monitors can display it with only so much chroma; there just isn’t enough light emitted to make it any more vivid. And the specifics of how each hue works are a little different, because our eyes don’t perceive all the various wavelegths equally.
Hue
Another thing to keep in mind is that the hue values are not exactly the same as HSL. All hues have been shifted up by around 30 degrees, though this amount varies a bit depending on the color. Here are gradients across all hues (0–360 degrees) in HSL and OKLCH for comparison:
Again, these differences come down to human perception, so some hues get a little more space on the spectrum. Additionally, these shifts are a little different when comparing across various brightnesses and saturation levels.
Some key color points are:
- Red: 30
- Yellow: 90
- Green: 140
- Cyan/teal: 195
- Blue: 260
- Magenta: 330
Hue can be expressed as an angle (25deg
) or as a number (25
).
Lightness
Another difference from HSL is that 0% or 100% lightness does not automatically mean full black or full white. If there is enough chroma, you can still get some color at these extremes:
oklch(100% 0.37 330deg)
oklch(0% 0.37 140deg)
If you want true black, white, or gray, make sure the chroma value is 0.
Lightness can be expressed as a percent (45%
) or as a decimal (0.45
).
Use a color picker
In general, as long as you keep the chroma below 0.37, you will usually be pretty safe. It’s pretty easy to specify a color that’s not in range of your monitor, but as long as your values are within reason, the browser should round it to a predictable result.
But when you really want to fine tune things, I suggest you use a color picker. The one at oklch.com is fantastic:
This picker provides conversions to most other color formats, so you can find fallback values easily. Shift-clicking the color swatch in your browser‘s DevTools also provides conversions, like it always has.
Instead of an rgb or hsl picker that you may be used to, you’ll notice the colors here aren’t displayed in perfect rectangles; it’s like chunks have been cut out of them. This indicates where colors are out of range of computer monitors. It’s a little odd at first, but it should start to make sense the more you play around with it.
Further reading: If you really want to dive deeper into understanding gamuts, color spaces, and the other CSS color functions, I highly recommend the High Definition CSS Color Guide by Adam Argyle.