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%:

slate blue
hsl(217deg 55% 50%)
bright spring green
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:

slate blue
oklch(55% 0.15 260)
bright spring green
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:

slate blue
oklch(55% 0.15 260deg)
grassy green
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:

slate blue
oklch(55% 0.15 260deg)
mossy green
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:

extremely vivid red
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:

extremely vivid orange
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:

HSL
OKLCH

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:

vivid light pink
oklch(100% 0.37 330deg)
very dark army green
oklch(0% 0.37 140deg)

Update: This behavior is the result of browser bugs and does not match the spec. Lightness of 100% should be pure white an 0% should be pure black. It has been fixed in Chrome, and will likely change in other browsers in the near future.

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:

OKLCH color picker

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.

Loading interactions…

Recent Posts

See all posts