What the color?!
CSS Color's Big Questions: Talk Introduction and Scope
Lea Verou opens by inviting the audience to reflect on the confusion surrounding recent CSS Color developments, asking why so many new color models exist and whether developers should bother with them. She frames the talk around a series of real-world mysteries — why OKLCH behaves unexpectedly at full lightness, why printed colors look washed out, why screens cannot reproduce a bright highlighter yellow — and promises to answer all of them. She also clarifies scope, noting the talk will not cover dark mode or the biology of vision, but will include the background needed to make the CSS-relevant material comprehensible.
RGB Origins: How Screens Work and Where Hex Colors Come From
Lea traces RGB color back to the first color screens in 1954, explaining that each pixel is made of three single-color light sources — red, green, and blue — each controlled independently like a dimmer. She explains that the 0–255 range and hexadecimal notation emerged directly from 8-bit hardware constraints, as the era demanded colors be specified in binary as close to the metal as possible. She then introduces device RGB and its core flaw: the same coordinates produce different colors on different screens because each display's primaries vary, setting up the need for something better.
Standardized Color Spaces and the Concept of Color Gamut
Lea introduces standardized RGB color spaces — sRGB, Display P3, Rec. 2020, Adobe RGB, and ProPhoto — which map specific coordinates to measurable, display-independent colors rather than raw hardware values. She reveals that all legacy CSS color functions (hex, rgb(), hsl(), hwb()) are defined within the sRGB gamut, meaning they do not exploit the wider range of modern screens. The segment concludes with a clear definition of color gamut as the set of colors a device or profile can represent, illustrated with a triangular gamut diagram, and explains why projectors lose so much color range under ambient light — a gamut reduction of around 80% by volume.
What Browsers Get Wrong: Gamut Clipping vs. Proper Gamut Mapping
Lea explains what happens when an out-of-gamut color is rendered: hardware clips overflowing values to zero or one, often producing a wildly different result — a yellow turning red, or a light color becoming garish and over-saturated. She contrasts clipping with proper gamut mapping, which preserves lightness and hue by reducing chroma to bring a color into range, and demonstrates a playground built during browser standards discussions where multiple algorithms were evaluated. Lea argues that browsers defaulting to clipping is a widespread and serious problem that affects far more everyday scenarios than developers realize, and she makes addressing it a central theme of the rest of the talk.
Perceptual Uniformity, Delta E, and RGB's Usability Failures
Lea introduces perceptual uniformity — the property that equal coordinate distances correspond to equal perceived color differences — and demonstrates that RGB lacks it dramatically, with color pairs of identical coordinate distance appearing wildly different to the human eye. She illustrates the practical consequence through a live color-matching game, showing how RGB requires frustrating multi-parameter fiddling with no clear hill to climb. Using an extended analogy to old-style faucets with separate hot and cold knobs versus modern single-handle designs, she argues that RGB exposes hardware internals rather than matching how humans naturally think about color — as a core hue with modifiers like lightness and saturation.
From HSL to OKLCH: Polar and Perceptually Uniform Color Spaces
Lea introduces polar color spaces, which represent color using a hue angle, a chroma axis, and a lightness axis, contrasting them with the rectangular RGB cube. She explains why HSL, despite being polar, still fails perceptual uniformity — two colors with the same HSL lightness value can look completely different in brightness, making it useless for reliable tint generation or contrast calculations. She then traces the evolution from CIE LAB (1976) through LCH to the modern OKLab and OKLCH, explaining that the 'OK' prefix signals bug fixes over LAB — particularly the blue-to-purple interpolation problem — and notes that gamut is the critical missing dimension in any comparison of these spaces.
The Dynamic Color Palette Dream: Vision, Failures, and Chroma Obsession
Lea shares her long-running goal of enabling dynamic color palettes in CSS — where a design system defines a few key colors and lightness levels, and all tints are generated automatically rather than hard-coded. She walks through her failed attempts to use OKLCH lightness manipulation to build such palettes, showing that the results are visually broken: hues shift, colors become unexpectedly saturated or desaturated, and a magenta becomes red. Lea then reveals a data-driven investigation into popular design systems (Tailwind, Open Props, Adobe Spectrum), where she found the same mysterious chroma curve recurring across all of them — and describes the obsession with reverse-engineering its secret formula.
The Gamut Clipping Revelation: Why OKLCH Palettes Break
Lea describes the key realization: every time she increased lightness in OKLCH, the color silently went out of gamut, and browser clipping was corrupting the result into something unrecognizable. She demonstrates with an interactive app that the in-gamut region of OKLCH is a surprisingly small and irregular shape that shrinks to a single point at maximum lightness, meaning any non-gray accent color will inevitably go wildly out of gamut as lightness rises — sometimes even beyond the ProPhoto gamut, which is wider than human vision. A second app shows side-by-side comparisons of clipped, gamut-mapped, and color-mixed results, confirming that proper gamut mapping recovers the intended color far better than either approach.
Implementing Gamut Mapping in CSS: Workarounds, None Values, and the Road Ahead
Lea explores whether gamut mapping can be implemented directly in CSS, walking through several approaches: iterative HSL saturation clamping, exploiting the CSS 'none' keyword to combine color components from two sources via color-mix(), and a recursive CSS @function — which currently fails due to a spec bug. She shows that for typical design-system chroma values the CSS workarounds approximate proper gamut mapping well enough to be useful, but acknowledges they are far too convoluted to distribute in production. Lea closes with a call to action: browser vendors prioritize features based on developer demand, so the most powerful thing attendees can do is loudly and publicly advocate for native CSS gamut mapping support.
There is a much better informed and nicer face about to open itself to you. So friends, Romans, countrymen, lend Lia Veru your ears. Lia Veru, give it up.
Hello, everyone. Wow, Bruce, that was a very fancy introduction. So it's so great to be back. This is one of my favorite conferences. I just love that it's all about CSS. I love how well organized it is. I just love being here, so this is great. Now, let's get on to color.
So raise your hand if you have been confused around all these developments in CSS Color in the last few years. Why why do we need all of this stuff? What are they good for? Should I care? Can't I just use HexColors? That's what my designer's giving me. Why should I worry about all this? So hopefully, if I do my job right, this talk is going to answer some of these questions.
When I started preparing this talk, my goal was to answer all of these questions and explain everything there is to explain about CSS Color. Turns out this was an ambitious goal, but hopefully I've covered enough ground. So, this is for you if maybe you've tried to use these new fancy OKLCH color models that have all these articles about them. And at first they seemed kind good, like yes, it's predictable, I can change the hue, the saturation or chroma doesn't change, I can change the lightness.
And then it was like, wait a second. Why is this white and this black, but if I increase the chroma even slightly, then it's not. It's something else. Like what is this? How is this 100% lightness? This should be white, right? So stay tuned and this talk will answer this question.
Also, will answer big some of life's big existential questions such as, why are my colors so washed out when I print something when they looked so bright on my screen? Or why is it that when I tried to match this bright yellow from my highlighter on a color picker on a screen, it didn't work and I couldn't find any combination of red, green, and blue or whatever you were using to actually match this.
Or why was the t shirt I bought a different color than what it looked like on the e shop? And I mean, this goes both ways. Sometimes it's not the color you hoped you would get, and sometimes you get a better color than you hoped you would get. So this is a dress I bought recently from Amazon.
And it turned out that actually, when opened it, it was my favorite color. This exact shade of turquoise is my favorite color. It is a color you cannot reproduce on a screen. I have not measured it with a spectrophotometer, which is the proper way to verify this. But I'm 99% sure that this is not a color you can reproduce on a screen properly.
And later in this talk we'll explain why. But in general, bright turquoises are notoriously hard. So it goes both ways. Sometimes you get a better color than what you wanted, but you wouldn't be able to tell. And also, it will explain why when you're projecting something you have a ton of lights around the screen, you can't see anything.
So yeah, let's get the lights down. Thank you so much. What this talk is not about, and these are not the only things it is not about, but some of the things it's not about. It's not about dark mode. I'm not going to touch on dark mode at all. Sarah is doing an entire talk about it. You should go to her talk.
It is not going to be about the physics or the biology of color. Like wavelengths, cones, human cones in your eyes and things like that. There's a very interesting talk somewhere about these concepts, but this is not the one. Because I wanted to keep it tightly related to things that you can use in CSS.
Things that are related. Things that are relevant to what we do. That said, there will be some background. Because to understand all of this, we first need to go back to the beginning. And the beginning was always RGB color spaces. Mean, not super far back to the beginning. Not before we had color screens.
But when we first got screens with color, which was surprisingly 1954. As early as that, believe it or not. I mean, obviously they looked a lot different. Surprisingly, they looked brighter than you might expect. And many people have this mental model that a screen is basically an array of these magic light bulbs that somehow produce millions of different colors. Like, you know, they have a white state, they have a gray state, they have a magenta state, or whatever.
All of these possible colors. The reality is, essentially each pixel is like three little light bulbs that can only display one color. Either red, green, or blue. And imagine that these light bulbs, each of them came with a little dimmer. And you could adjust these dimmers differently for each color.
That is what gets us all the different colors that we see. And different screens have different architectures, but essentially, this mechanism has remained largely unchanged since the first color screens since the first color screens in the fifties. Like, we turned all the dimmers down to produce black, we just turned the red one to produce red and so on.
This is yellow. And this you probably can't see it on the slide, but these are different. So now there are a lot of these light bulbs in digital screens. If you think about the HD or even worse, the four worse, better. The four k screens that we have today, that is a lot that is several millions of these tiny light bulbs.
And even back then, it was hundreds of thousands. So back before we could have proper processing power and proper graphics cards, things needed to be very, very efficient. Especially a computation that applied to every single pixel, that was very expensive. We needed to be fast.
That was the top priority. So when we needed to specify a color, it had to be as close to the metal as possible. Like basically exactly specifying the levels of the dimmers down to the binary. Each of these had eight bit supported eight bits, 256 states.
And that is where the range that we that we learned that RGB goes from zero to 255. That's where it comes from. That's where the hex colors come from. They're just a compact way to write binary. It's basically that if you use hex, you can compress four digits of binary into one digit of hex. But it's basically a way to represent binary.
So hex colors came from this era where we needed to be so efficient that we were essentially specifying the colors in binary based on the hardware that the screen understood. It had several problems. The advantage was obviously performance. It was super fast. I mean, for the standards of the time, what you could do back then.
But every screen supported different capabilities. And when you use device RGB, which is what this is called, basically when you just throw coordinates on the screen, you get a different color on each screen. And that is why if you haven't taken any extra care to account for this, then you see different colors for the same coordinates.
This is a general point. This is not about CSS, which we will get to very soon. So, people often discuss RGB as if there is the one RGB and they're like, oh, this is RGB, these coordinates. Or like, I hope this is like RGB, whatever. And they exchange hex colors. And like, you can copy a hex code from one application to another. And it's like, nobody talks about what does this mean.
Because there are many different RGBs. So there's the device RGB, which is the close to the metal what the hardware understands, which is actually a different range depending on the hardware. What we've learned is zero to two fifty five.
But actually modern p three screens support 10 bits per component. Why? Because they have a wider range of colors they can represent. So if it was still eight bits, we would get big jumps between them. So they support 10 bits per component. So if we were actually representing it in device RGB, which we cannot do in CSS, we don't have access to that.
But if we could, it would actually look more like this for a p three screen. Instead of two fifty five, it would go up to ten twenty three. Which is why instead we're switching towards using a zero to one range, even when we are using RGB. Or a zero to 100%, you might see it. And that abstracts away at least one factor.
Like that abstracts away the bit depth. But it still means different things on different screens, because the primaries are different. The intensities of the red, green and blue are different across screens. So this is why we got standardized RGB spaces. So what is a standardized RGB space? And now the first one I think was sRGB. Or definitely the most popular one.
So instead of being tied to hardware and just throwing raw RGB values at the screen, These color spaces not only use zero to one ranges, but also each triplet of coordinates corresponds to a specific measured color. And as long as your display can't show that color, it will be the same color across every display. So you can see here how the device RGB coordinates that are the same across both of them, even though we're seeing a completely different color.
Actually, are we seeing? Yes, you can actually see the difference there. It's a bit less than it is on my screen but you can. See what lowering the lights can do? So as you can see, even though they have the same coordinates in device RGB, they have different coordinates in sRGB. And some of you may have noticed something else which we'll come back to very shortly.
And then we have other spaces. We have display P3, which is wider. We have Rec twenty twenty, even wider. There is others, Adobe RGB, ProPhoto. ProPhoto is so wide that it encompasses the entirety of human vision. And actually its primaries cannot be seen by the human eye. CSS does support that. You know, if you ever wanted to to be fancy.
So all legacy CSS colors are actually in sRGB. People assume that if they use red, then it is their screen's brightest red. That it will basically throw one hundred percent zero zero at the screen. That is not the case. It is sRGB red. Which I suspect most of you probably have a reasonably high end screen. It will be more grayish than the maximum red that your string can reproduce. Same with text, same with the RGB function, same with the HSL function.
And yeah, who remembers the HWB? Has anyone ever used HWB? Ever? Like outside of a demo. Yeah, no hands. I've used it but only for demos and and ColorJS and things like that. So, yeah, anyway, HWB also these are very simple transformations over an RGB space.
And in CSS, they're defined to be over sRGB. So, people have been using this fact to say, oh, but you cannot do white gamut colors with these functions. Technically, you can. It's just very annoying. And I do not recommend it.
Which is why I did not include it though as a pro or con of these. So, I said that some of you might have noticed something else. When we converted the p three magenta to sRGB, we got these weird values. They're out of range.
They're bigger than one? How how can we have a dimmer that goes more than a 100%? How can we have a dimmer that goes below 0%? Like, is it sucking voltage out of the atmosphere somehow? So this is how we represent colors that are out of out of gamut. And each RGB space has a gamut attached to it.
So, the gamut is the range of displayable colors. And it basically depends on the primaries, on the brightness of your pure red, pure green and pure blue, since every other color that you produce is basically a linear addition of those. So, the brighter these are, the bigger your gamut.
Which is why often we represent gamuts as this triangle. This is a very very abstracted representation. But it's basically trying to show where the gamuts stand relatively to each other. The gradient behind is not accurate in any way. It's just there to show you roughly where are the blues, where are the reds, where are the greens.
That is the only thing it's good for. So if any component is outside zero to one, the color is out of Gammut. You can even use this in relative color syntax to do fancy things with out of Gammut colors. Why would you want to do that? Gammut explains color gamut explains why when you print something it's much more washed out. It it all makes sense when you see the actual gamut of a printer.
Printers use CMYK, so their gamut is not a triangle because they have more components. But it's basically it's way smaller even from sRGB, even from a MacBook the MacBook Air to 2013 that I've been using in these slides as an example of a laptop with a very small color space. I actually used to have that laptop in twenty thirteen.
And at some point I discovered how small its gamut was and I was horrified. And ever since I downloaded its color profile and I've been keeping it around just for these types of demos. And it also explains the projector thing. Why can't we can we not see anything when there's a lot of ambient light around the projection?
So it having a lot of light around the projection decreases its gamut by down to about a sixth, depending on the light obviously and the details. But a sixth by volume, that is a huge decrease. It's like 80 something percent. So, that is very, very important.
And as we will see later throughout this talk, I think it's one of the most important things that have to do with CSS Color today. So we've seen how when we convert a color, an RGB color, to a certain RGB space and it's out of gamut, it will have values outside of the zero to one range.
What happens if we try to render these colors? Like, suppose we throw them on the screen, we render them. Like, how should we render them? So, there are multiple things you could do there. This is an example of an sRGB out gamut color.
It is not tremendously well, I suppose it is quite out of gamut, but yeah. And if you just throw the coordinates at the screen without doing anything else to them, the screen will just clip them. I mean, think of it that way. It has a certain number of bits. You are sending a number of bits that is higher.
What is it going to do? It's going to overflow. It's going to reject it. So if negative, basically the way that the hardware does it is it just converts it to zero. And if it's over one, it clips it to one. Or, I mean, the maximum. Whatever that is. So, this is where a process called gamut mapping comes in.
Because as you can see, what we had and what we ended up with when we clipped was significantly different. So, the second swatch is actually mapped to preserve hue and lightness as much as possible. And as you can see, what we actually had here was kind of a yellow and we ended up with a red when we clipped. Guess which one browsers actually do today?
Yep. Browsers do clipping. And one of my goals with this talk is to get people to realize how important it is to stop doing clipping and to actually do it properly. Because there's this widespread belief that, oh, getting out of gamut is something that just happens in edge cases when you do weird shit.
No. It happens all the time and I'm gonna show you this. So there's multiple different ways to gamut map. So basically, most gamut mapping algorithms focus around preserving some aspect of the color. Usually, want to preserve first the lightness, because if we change the lightness too much, you could end up having accessibility problems as well. And that is a big problem with clipping, because you often end up with colors that have a wildly different lightness.
And secondarily, we try to preserve the hue. Usually the chroma is the one the chroma saturation is the one that is most tolerant of changes. Like the color still preserves its general characteristics. You can visit this page on that URL and play with sliders here. Ignore the gradients, like this color picker is not designed for these ranges. But basically, you can see how we started with a color that had 90% lightness.
That is a fairly light color. And clipping gets us this. That is not a 90% lightness color. And now, there's many different algorithms with different characteristics, different performance. This is a playground that many standards people collaborated on when we were discussing gamut mapping and what gamut mapping algorithms should browsers implement.
Many were proposed that made different trade offs of quality of the result, performance and so on. But generally, anything, anything is better than clip, as you can see. And you can like play around with the sliders. I mean, clipping, there are some very rare cases where it can be good. It can preserve chroma when you're very close to the gamut, but you're just slightly outside.
But when you're significantly outside then it has very poor results. RGB color spaces, all of them, have another problem which is called lack of perceptual uniformity. Which sounds fancy. But it's simpler than it sounds. Bear with me. So it basically means, any color space, including RGB, is basically a three-dimensional coordinate space, right?
And colors are points on that space. So, as you know from geometry, when you have a three-dimensional space, you can use Euclidean distance to get a measure of the distance between the two colors. Now, perceptually uniform color spaces, that means something.
That distance actually correlates with how different the colors actually look. In RGB, and other spaces that are not perceptually uniform, that difference is not very meaningful. Like both of these color pairs have the same distance in RGB, 0.87. But they're actually significantly Their differences are significantly different.
Which you can see by using a measure called delta E. And same here. All of these are Each step has the same distance in terms of coordinates. But the perceptual difference is much more significant, right? The first and the second color look quite relatively similar.
The where and the third the third and the fourth as well. Whereas the second and the third have a much bigger difference. And that is why it is not a good idea to use RGB for gradients and generating color palettes or measuring proximity and doing things like that. This I think is even more dramatic.
Look at that. It's like 19 versus 40. And you can see it with your eyes as well. Right? So, these the only problems with RGB? No. There is also a usability issue. As you may have found, I don't know if you remember when you were starting and you weren't very familiar with RGB and you were trying to match a certain color, like adjust the color, make it lighter, write the color that kind of approximates what you had in mind. So I've made this little color game where you can select a color space and then try to match a random color.
And I find that RGB, it is always much harder. Like, you kind of you kind of go a certain way, then it's like, no, need to go that way. Oh, no, I actually need to go like the other way. And it's like you keep fiddling.
And I'm sure eventually, you get it, But it's not a nice experience. There is no hill climbing there, I got it. That was actually fairly fast. See, it's it's Stockholm syndrome. If you spend enough time with a bad UI, you get used to it. But it's a little bit like that. Like you it does not good UIs kind of progressively guide you towards your goal. Bad UIs, you tweak one parameter and other parameter breaks. Can you think of any other similar UI you may have used?
So totally different domain, but taps or faucets, depending on which country you live in, have the same problem. There is the older faucets which have two different knobs. One for hot water and one for cold water. And they also these knobs affect basically how much water of that temperature is coming out of the tap.
So one of them gets you more or less hot water, and the other one gets you more or less cold water. So what happens if you want a certain flow and a certain temperature, which is usually your use case? You have to tweak them endlessly until you find the right spot. And of course, then you cannot save it.
Contrast that with newer taps that have separate controls for these two things. So you turn it to select the temperature, and you move it up and down to select the flow. And if you find the right temperature, you can still turn it off and it remembers the temperature because it is a different setting. It's the angle. But mainly, it actually follows the user's mental model.
It's designed around the user goal. And this is something that permeates good UIs versus bad UIs in every domain. From color to faucets. Like, good UIs are designed around what are people actually trying to do with this. Bad UIs are designed around what is my implementation?
What is my hardware? I'll just expose this directly because that's easy. Like the second faucet has to do more work. Internally, it still works like the first one. It has to translate what you've selected into levels of cold and hot. It just doesn't put that that burden on the user. Because for this kind of mechanical computation, it's machines that are much better than us.
So RGB has exactly that problem. Because of its hardware origins, because it was designed around how screen hardware worked, it does not take into account how humans think about color. Like if I asked you to describe this color to the person next to you, actually let's take a minute and do that.
Can you describe this color to the person next to you? Suppose you were talking to your, like, partner and telling them I want you to buy a t shirt of that color. Alright. So I'm sure there were many different descriptions. And there have been studies on this. And they all find different, slightly different adjectives.
But basically, the way that humans describe color is either some kind of name like top or whatever. Or leather or whatever. Or when it's described as a system it's like a core hue, a core color and modifiers. Like light muted brown, or dark beige.
Or like light brown or whatever. Or lighter, darker, muted, bright, things like that. It is never ever about 60% red and a little bit less green and even less blue, but combine them like you're combining lights, not like you're combining paints.
Like nobody ever says that. So, when you're using RGB to specify color, you have to do the mental translation, just like when you're using the older faucet. You have to think in terms of how humans think and translate it into the RGB coordinates. I mean, yes, we have tools for that. But there are always cases where you need to tweak coordinates.
When you need to create variations, when you need to dynamically do something. Or even just when reading code to understand what's going on there. I mean, you don't always have a preview. So to solve this problem, we got polar color spaces. So why do we call them polar? Because they have an angle. The angle represents the hue.
So instead of having this is in contrast to rectangular color spaces like RGB, which are shown like a cube. So, polar color spaces have an angle that shows what is the the core type of this color.
And then Okay. And then another coordinate. Another coordinate usually that controls how gray the color is or how bright it is. In HSL it's called saturation.
And then a third one that controls how light and dark it is. And CSS supports several of these with different characteristics. And we will soon see what these characteristics are. So HSL sounded really good at first.
It's a very old color space. It's a very simple transformation over RGB. Very simple math. Could even be done in old hardware. But it means nothing. Like the coordinates mean nothing when you're comparing different hues or different saturations. Like, just to give you an example, both of these have a lightness of 50%. That makes no sense.
These are not equally light colors. Which means you cannot depend on the lightness meaning anything. You cannot use it to create tints and shades, you cannot use it for contrast calculations, nothing. So, enter perceptually uniform color spaces, which are very old. Some of them are really old, like starting from 1976 with LAB.
But it's only recently that we were able to use them in CSS. So, what is a perceptually uniform color space? It's basically it doesn't have the problem I showed you earlier. The distance between coordinates is the perceptual distance. It's basically what you see. And the polar coordinates like hue or chrome and lightness actually mean the same thing. You can tweak lightness across the space and the theory is it means the same thing.
And also, in HSL there are cases where if you increase the lightness, perceptually you're also decreasing the saturation, things like that. Or sometimes you change the hue, it changes the perceptual hue. In these spaces, the polar coordinates are orthogonal. Like if you change the hue, it only changes the actual quality of the color. If you change the chrome, it only changes how gray it is. If you change the lightness, it only changes how light or dark it is.
Again, big asterisk there with a big caveat. But why? Before we get to the caveat, why? Why would we do that? Obviously, to create nicer interpolation, nicer gradients, nicer transitions, you know this stuff. To make it easier to create dynamic palettes, color palettes or ramps where you have like a core accent color, and you create tints that are lighter or darker and you can skin entire components.
That was the theory we were sold at least. And other dynamic computations like generating a contrasting color that's automatically contrasting. Like just as an example, it is not the focus of this talk. But even though contrast color just became baseline, if you can depend that the likeness of a color space actually means something, you could maybe you disagree with the user agent's value.
Or maybe you want a fallback for browsers that go, where contrast color is not supported. Whatever it is, you can actually decide on your own threshold. And as you can see, here I've picked 0.7, which favors white a little bit more. And there are cases where it can produce better results, depending on the specifics.
Let me see if I can find one of these cases. Like for example, I think the white is much easier to read there. Woah, even worse there. So, there you can make your own choices. That means it must be a lightness you can depend on. Imagine if you were doing this with HSL.
You can do it. HSL does support relative color syntax. But imagine doing that with HSL. Yellow and blue would get the same color. Another question that people frequently ask is, why do we need the okay versions? What does that mean about lab and LCH? Are they the crap versions? So, I mean, they were very good when they came out for what they were. They are generally perceptually uniform.
Generally, they do have a bug around blues. So if you notice when you go from white to blue in LAB, you're getting purple in the middle. This is a known problem with with LAB. That was exactly why Okay Okay LAB was designed.
It is Okay LAB is basically LAB with some bugs fixed. And the Okay LAB version is much better than both of these. So just as a summary. It is each of these spaces does have its pros and cons.
But one of the things that is missing from this table is gamut. And that is the big pain point. And I stumbled on this problem, I've been trying to solve this problem of creating dynamic color ramps for a very long time, very, very long time. Because basically it really bothers me that today we have to do all of this to define colors for a website.
Most of these are generally programmatically generated with some tweaking, but still we have to do it, we need to include all of this just to have colors that we can depend on. And then because we don't have primitives in CSS to go from one to the other, everything's hard coded. And my vision is it would be so great if all we needed was basically define our levels, define our key colors.
Like what would you use as an accent color? What's the peak saturation? Like if you had to color a primary button with the brightest color in the scale, what would it be? And it doesn't even need to be in the scale. And then if just given these, I wanna be able to generate these. So can we do that?
Like, my vision is that basically the design system, the page would define its core colors and its likeness levels, and then each component would define how these are allocated. And you can do that if if this actually worked. Like, the story we were told is, you know, you change the lightness in an OKLCH color and you get something with the same characteristics, just lighter or darker.
It should work in theory, right? And doesn't it look really beautiful? You can even clamp to the lightness to have certain constraints, like, you know, the icon I wanted. I do want to use the accent color mostly, but I still want to make sure it's not super light, and things like that. And then variations become cheap. All you have to do is change an accent color and the component just adapts. They can even be created on the fly.
I have a brand color. I'm going create a variation. Today, all these variations have to be hard coded because they require there's such a combinatorial explosion. I'm not even going to cover nested callouts. So, I tried to do that, and this was the result. These 10 to 90 are not lightnesses.
They are these abstracted lightnesses. They are basically these levels that I showed you there. And you know, if you glance at it without looking too carefully, it kinda doesn't look too bad. But then you try to use it on an actual element, like on an actual component, on an actual call out, and all the horror. It's actually kinda more decent there.
But it's they're basically some of them are extremely bright. Some of them have changed hues. Like, look at this purple. It became pink here. Look at this gray. It basically became like a dark like a desaturated blue cyan or something. The magenta became red.
It's just totally not what like, this is unusable beyond a demo. Right? You cannot use this on an actual production website. And it it for for the longest time, I it drove me crazy. Why didn't it work? The question haunted me why? Why it should work? What is it? It became an obsession.
I even built a website to get data from popular color palettes that were tweaked by a designer to notice patterns in the data. And you can access it as well. This is the URL. You can click, for example, on Tailwind. And you can see charts of where the different color swatches stand on each coordinate. You can even tweak the coordinates yourselves.
I'm not going to cover hue. Sometimes the hue changes itself. But the main thing, the thing that obsessed me the most, is chroma. There is a certain shape, a certain curve that goes kind of like this, that you see over and over again in almost all of them. Like let's go to wide open props. Same curve. Open color.
Very similar curve, just kind off. Of Alder Tailwind, very similar curve. Adobe Spectrum, kind of a similar curve. Right? So I thought that the key to succeeding at this task was to manage to to unlock the secrets of this this curve.
There there there must be a secret like formula there. I gaslit myself about what chroma means, even. I was thinking, you know, maybe what we actually want is not just to change the lightness.
We think we want to change the lightness, but what we actually want is also to lower the chroma. And I started looking, like, often people will create dynamic color palettes by mixing white and black. It does prevent the super saturated problem that we saw earlier, but it creates the opposite problem that they're often very washed out.
And we have no guarantee about each tint. Like, we cannot use color mix to create a color palette and end up with like 40 or 50, for example, having a predictable lightness that we can use in computations. It's only relative to the key color, just like HSL. And so then, some of my early experiments, which you can see here, let's see this loading.
Some of my early experiments, which you can see here involved, maybe we just need to combine the two approaches. Like, maybe we have the color mix color, and the color with the modified lightness, and then we just like find the perfect mix between them. It sort of worked.
It worked better than just plain color mix because it gave it some saturation. There was no ideal mix. There was no value that gave you what you wanted for every possible color. It was just slightly better. And one day, it dawned on me. After an embarrassingly long time of trying to figure this out, I noticed there's this color picker component that I've built, and you can access it as well.
And I noticed that whenever I increased the lightness, the color got out of gamut. Any and that held true for any accent color. Let's go here, sRGB even. At some point, will get out of gamut. Guaranteed. So I built an app to explore this hypothesis, like could the problem be gamut again?
Could it be gamut clipping showing its nasty little head? So, I built this app that lets you move along OKLCH and see how the gamut actually adapts. And here's the thing. If you show all the colors and you just try to create a hue wheel, then it kind of looks nice, and you've seen a lot of demos, you might have seen a lot of demos with this sort of thing, but then you go lighter and it's like, what? This is the perceptually uniform space I was promised?
This is not perceptually uniform. How is this color the same lightness as this? I mean, this is supposed to be a slice of constant lightness. And it made no sense, but it all becomes clear if you actually clip, if you actually hide the out of gamut colors. So the out the in gamut colors are actually a much smaller shape, and it becomes much much much smaller at the edges.
At a 100%, it's only a single point, And that point gives you weight. Anything outside of it is clipped or mapped. And if you're not doing mapping, you're going to get the wrong color. So, there is this widespread belief that if you don't do weird things, you're not going end up with out of gamut colors.
And that is, as you can see, patently not true. Pick any accent color here, any of them, any, like with whatever chroma you want, and make sure it's in gamut, and increase the lightness. Whoops, you're out of gamut now. The only thing that changes is when you get out of gamut. But you will eventually get wildly out of gamut.
The p p plus here means outside pro photo. And remember pro photo is is wider than human vision. So we're not even talking about a small difference. You will get wildly out of gamut. And to test this hypothesis further, I built this other little app, where you can see what the browser is doing.
You can select different colors. You can see what the browser is doing, which is clipping. So these two are going to be the same visually. And then how does it look with gamut mapping? And as you can see, yeah, it's not perfect. Yes, you still need to do some tweaking. But boy, it's so much better. Like you don't get this weird super saturated light tint.
And you can also see how grayed out the color mix version is. And there is and here is the mixed the mixed one, because I I didn't give up on that idea completely, I still tried it out. Still doesn't work. And you can click on different colors here and see what happens.
But in general, the gamut mapped version is always way, way better. In some cases it's basically there. In other cases it made quite a little bit of tweaking but it's basically there. And look at color mix how it's also giving you a different hue sometimes, again the blues. So, when we talk about perceptually uniformed faces, when we talk about device independence, when we talk about orthogonal orthogonal coordinates, all of that is when you're in gamut.
When you go out of gamut, it's the wild west. All of these fly out of the window. So, it is extremely important to do proper gamut mapping, so you get a color that maintains some guarantees about what you were trying to do. So then, once I realized that gamut mapping is the solution, is the answer, and it gets you like 90% of the way there, my next thought was, can I do it in CSS?
Can I implement this in CSS? Because obviously I can do it in ColorJS, but who wants to to have to do that? I wanted to have dynamic tins. Like this is my life's mission. I need to be able to create dynamic tints. So, it must be possible to do in CSS. The first thing you need to keep in mind is no matter the lightness and the hue, there will always be a chroma that brings the color in gamut.
No combination of lightness and chroma or chroma and hue has this property. But for chroma, there is always a value that is in gamut. Like let's go here, for example, we can just reduce the chroma and it's in gamut. And whatever color you have, there is a value that is out of gamut. So basically, most gamut mapping algorithms just try to find what's the maximum chroma that still has this property, that still maintains in gamut. And some of my early attempts may have been, shall we say, convoluted. This is real code that I thought at some point that I would ship.
I did not ship it, thankfully. I got to my senses. It was basically it looks insane, but there was a logic to it. I was actually trying to match the gamut boundary by combining different functions. Like, I had multiple functions and I was using max and min to combine them, to basically add them together.
But eventually I realized there is a simpler way. What if we just take the color, convert it to HSL, clamp its saturation, convert it back, then replace the lightness we want and repeat until it settles. So I tried that. And it settles surprisingly fast.
Like, it takes four or five iterations usually. But then I thought, this works when you need to set a certain tint, like when you have the lightness already. Because look here, we are actually setting the lightness. Like we have a fixed lightness that we're setting back. Here we're using OKLCH to get a relative color that sets that particular lightness. What if we don't have the lightness and all we have is a color that we need to gamut map?
So, there is a slightly different variation of that where we basically convert to HSL, clamp, then do this weird trick with color mix. So to replace the original hue and lightness into that color, convert it to OKLCH, and then plug the the hue and lightness back in. And you might be puzzled at this point, But bear with me. So, slight detour.
There is one of my So I proposed relative color syntax back in 2016 or so. And it got implemented and it gave us all these abilities, yay. I recently kept coming across cases where you actually want to combine more than one color. So I proposed relative color syntax with two colors. It got accepted by the working group.
It's not there yet. It's not even in the spec. So what can we do until then? It won't work for every case, but for some cases there is a workaround. And the workaround is based on non values. So non values were originally designed to represent grayscale colors. Like if you convert a grayscale color to HSL or any other polar format, it will automatically get a none for the hue.
But you can actually use none yourself for any component. And what does it mean? It means when you interpolate that color, the none gets the value of the other color there. Whatever value the other color has in that component, it gets that. And if that's also none, it still gets none. So as you can see, if we mix this with zeros, we get a gray. Like if we mix this magenta with zeros.
If we mix this magenta with the same lightness and none, we get a darker version of that color. And we can use that, because we can take a color, we can use relative color syntax to replace some of its coordinates with none, and then we basically get a color that has essentially little holes where these components used to be, and then when you color mix them, these holes get values.
So, that's basically where this that basically allows you to combine components from two different colors using color mix. Just make sure you do not incur any space conversion. They both need to be in the same color space. Because if you convert a color that has none to a different color space, you usually lose the none's and they become zero's and you get a completely different color.
So, important to convert them to that color, the color space you're mixing in before replacing it with none. And you can also do linear variations, like 30% of that component and 70% of the other by nesting color mixes or doing things like that. So, that's basically what this is based on.
It just combines the components of these two colors. And that also settles relatively fast. So, thought, is there something there? And plugged it into the gamut mapping playground. For the types of colors that we usually have when we're doing design systems, which don't go above 0.4 chroma, it worked pretty well. It worked as well as the other much fancier algorithms.
So, the problem is, it is very hard to distribute something like this. If you can express the tint with a relative color modification, then you can just pass it around and you tell people, just use OKLCH from your accent color and then use this variable. And magic, you get a tint. But when it's multiple nested functions, it's very awkward.
So I thought, when we have function, can we use that function? I tried to do that. In fact, tried to do a recursive thing where it called itself. It did not work. Turns out recursive at function, not a thing right now. But it is considered recursive even if the value itself has gone through the same function.
So even if I When I wrote it as two separate functions, because I'm calling gamut map iteration over the same value and then basically wrapping it, still doesn't work. The good news is it's a bug in the spec. It will be fixed. Bad news is it doesn't work right now even in Chrome. So, bottom line of this is, yes we have workarounds.
But damn we need gamut mapping. We need it to work. We need to be able to not have to do all this stuff. So go out there and ask ask browsers to do this because that's the only thing that works. If they hear developers requesting something, they prioritize it. Trust me. That is the biggest thing you can do to get something prioritized. Complain loudly.
That was it. Thank you so much.
Technologies & Tools
- color-mix()
- ColorJS
- contrast-color
- Hex Colors
- Open Color
- Open Props
- Relative Color Syntax
- Tailwind
Standards & Specs
- Adobe RGB
- CMYK
- Device RGB
- Display P3
- HSL
- HWB
- LAB
- LCH
- OKLab
- OKLCH
- ProPhoto RGB
- Rec. 2020
- RGB
- sRGB
Concepts & Methods
- Color Gamut
- Delta E
- Gamut Clipping
- Gamut Mapping
- Perceptual Uniformity
- Polar Color Space
Organisations & Products
- Adobe Spectrum
Modern CSS color has grown a lot of new features — and with them, a lot of
confusion. Lea Verou digs into where CSS color capabilities genuinely help,
where they cause more problems than they solve, and what workarounds and
upcoming solutions are worth knowing about.















