3 posts on CSS WG

LCH colors in CSS: what, why, and how?

7 min read 0 comments Report broken page

I was always interested in color science. In 2014, I gave a talk about CSS Color 4 at various conferences around the world called “The Chroma Zone”. Even before that, in 2009, I wrote a color picker that used a hidden Java applet to support ICC color profiles to do CMYK properly, a first on the Web at the time (to my knowledge). I never released it, but it sparked this angry rant.

Color is also how I originally met my now husband, Chris Lilley: In my first CSS WG meeting in 2012, he approached me to ask a question about CSS and Greek, and once he introduced himself I said “You’re Chris Lilley, the color expert?!? I have questions for you!”. I later discovered that he had done even more cool things (he was a co-author of PNG and started SVG 🤯), but at the time, I only knew of him as “the W3C color expert”, that’s how much into color I was (I got my color questions answered much later, in 2015 that we actually got together).

My interest in color science was renewed in 2019, after I became co-editor of CSS Color 5, with the goal of fleshing out my color modification proposal, which aims to allow arbitrary tweaking of color channels to create color variations, and combine it with Una’s color modification proposal. LCH colors in CSS is something I’m very excited about, and I strongly believe designers would be outraged we don’t have them yet if they knew more about them.

What is LCH?

CSS Color 4 defines lch() colors, among other things, and as of recently, all major browsers have started implementing them or are seriously considering it:

LCH is a color space that has several advantages over the RGB/HSL colors we’re familiar with in CSS. In fact, I’d go as far as to call it a game-changer, and here’s why.

1. We actually get access to about 50% more colors.

This is huge. Currently, every CSS color we can specify, is defined to be in the sRGB color space. This was more than sufficient a few years ago, since all but professional monitors had gamuts smaller than sRGB. However, that’s not true any more. Today, the gamut (range of possible colors displayed) of most monitors is closer to P3, which has a 50% larger volume than sRGB. CSS right now cannot access these colors at all. Let me repeat: We have no access to one third of the colors in most modern monitors. And these are not just any colors, but the most vivid colors the screen can display. Our websites are washed out because monitor hardware evolved faster than CSS specs and browser implementations.

Gamut volume of sRGB vs P3

2. LCH (and Lab) is perceptually uniform

In LCH, the same numerical change in coordinates produces the same perceptual color difference. This property of a color space is called “perceptual uniformity”. RGB or HSL are not perceptually uniform. A very illustrative example is the following [example source]:

Both the colors in the first row, as well as the colors in the second row, only differ by 20 degrees in hue. Is the perceptual difference between them equal?

3. LCH lightness actually means something

In HSL, lightness is meaningless. Colors can have the same lightness value, with wildly different perceptual lightness. My favorite examples are yellow and blue. Believe it or not, both have the same HSL lightness!

Both of these colors have a lightness of 50%, but they are most certainly not equally light. What does HSL lightness actually mean then?

You might argue that at least lightness means something for constant hue and saturation, i.e. for adjustments within the same color. It is true that we do get a lighter color if we increase the HSL lightness and a darker one if we decrease it, but it’s not necessarily the same color:

Both of these have the same hue and saturation, but do they really look like darker and lighter variants of the same color?

With LCH, any colors with the same lightness are equally perceptually light, and any colors with the same chroma are equally perceptually saturated.

How does LCH work?

LCH stands for “Lightness Chroma Hue”. The parameters loosely correspond to HSL’s, however there are a few crucial differences:

The hue angles don’t fully correspond to HSL’s hues. E.g. 0 is not red, but more of a magenta and 180 is not turquoise but more of a bluish green, and is exactly complementary.

Note how these colors, while wildly different in hue, perceptually have the same lightness.

In HSL, saturation is a neat 0-100 percentage, since it’s a simple transformation of RGB into polar coordinates. In LCH however, Chroma is theoretically unbounded. LCH (like Lab) is designed to be able to represent the entire spectrum of human vision, and not all of these colors can be displayed by a screen, even a P3 screen. Not only is the maximum chroma different depending on screen gamut, it’s actually different per color.

This may be better understood with an example. For simplicity, assume you have a screen whose gamut exactly matches the sRGB color space (for comparison, the screen of a 2013 MacBook Air was about 60% of sRGB, although most modern screens are about 150% of sRGB, as discussed above). For L=50 H=180 (the cyan above), the maximum Chroma is only 35! For L=50 H=0 (the magenta above), Chroma can go up to 77 without exceeding the boundaries of sRGB. For L=50 H=320 (the purple above), it can go up to 108!

While the lack of boundaries can be somewhat unsettling (in people and in color spaces), don’t worry: if you specify a color that is not displayable in a given monitor, it will be scaled down so that it becomes visible while preserving its essence. After all, that’s not new: before monitors got gamuts wider than sRGB, this is what was happening with regular CSS colors when they were displayed in monitors with gamuts smaller than sRGB.

An LCH color picker

Hopefully, you are now somewhat excited about LCH, but how to visualize it?

I actually made this a while ago, primarily to help me, Chris, Adam, and Una in wrapping our heads around LCH sufficiently to edit CSS Color 5. It’s different to know the theory, and it’s different to be able to play with sliders and see the result. I even bought a domain, css.land, to host similar demos eventually. We used it a fair bit, and Chris got me to add a few features too, but I never really posted about it, so it was only accessible to us, and anybody that noticed its Github repo.

Why not just use an existing LCH color picker?

  • The conversion code for this is written by Chris, and he was confident the math is at least intended to be correct (i.e. if it’s wrong it’s a bug in the code, not a gap in understanding)
  • The Chroma is not 0-100 like in some color pickers we found
  • We wanted to allow inputting arbitrary CSS colors (the “Import…” button above)
  • We wanted to allow inputting decimals (the sliders only do integers, but the black number inputs allow any number)
  • I wanted to be able to store colors, and see how they interpolate.
  • We wanted to be able to see whether the LCH color was within sRGB, P3, (or Rec.2020, an even larger color space).
  • We wanted alpha
  • And lastly, because it’s fun! Especially since it’s implemented with Mavo (and a little bit of JS, this is not a pure Mavo HTML demo).

Recently, Chris posted it in a whatwg/html issue thread and many people discovered it, so it nudged me to post about it, so, here it is: css.land/lch

FAQ

Based on the questions I got after I posted this article, I should clarify a few common misconceptions.

“You said that these colors are not implemented yet, but I see them in your article”

All of the colors displayed in this article are within the sRGB gamut, exactly because we can’t display those outside it yet. sRGB is a color space, not a syntax. E.g. rgb(255 0 0) and lch(54.292% 106.839 40.853) specify the same color.

“How does the LCH picker display colors outside sRGB?”

It doesn’t. Neither does any other on the Web (to my knowledge). The color picker is implemented with web technologies, and therefore suffers from the same issues. It has to scale them down to display something similar, that is within sRGB (it used to just clip the RGB components to 0-100%, but thanks to this PR from Tab it now uses a far superior algorithm: it just reduces the Chroma until the color is within sRGB). This is why increasing the Chroma doesn’t produce a brighter color beyond a certain point: because that color cannot be displayed with CSS right now.

“I’ve noticed that Firefox displays more vivid colors than Chrome and Safari, is that related?”

Firefox does not implement the spec that restricts CSS colors to sRGB. Instead, it just throws the raw RGB coordinates on the screen, so e.g. rgb(100% 0% 0%) is the brightest red your screen can display. While this may seem like a superior solution, it’s incredibly inconsistent: specifying a color is approximate at best, since every screen displays it differently. By restricting CSS colors to a known color space (sRGB) we gained device independence. LCH and Lab are also device independent as they are based on actual measured color.

What about color(display-p3 r g b)? Safari supports that since 2017!

I was notified of this after I posted this article. I was aware Safari was implementing this syntax a while ago, but somehow missed that they shipped it. In fact, WebKit published an article about this syntax last month! How exciting!

color(colorspaceid params) is another syntax added by CSS Color 4 and is the swiss army knife of color management in CSS: in its full glory it allows specifying an ICC color profile and colors from it (e.g. you want real CMYK colors on a webpage? You want Pantone? With color profiles, you can do that too!). It also supports some predefined color spaces, of which display-p3 is one. So, for example, color(display-p3 0 1 0) gives us the brightest green in the P3 color space. You can use this test case to test support: you’ll see red if color() is not supported and bright green if it is.

Exciting as it may be (and I should tweak the color picker to use it when available!), do note that it only addresses the first issue I mentioned: getting to all gamut colors. However, since it’s RGB-based, it still suffers from the other issues of RGB. It is not perceptually uniform, and is difficult to create variants (lighter or darker, more or less vivid etc) by tweaking its parameters.

Furthermore, it’s a short-term solution. It works now, because screens that can display a wider gamut than P3 are rare. Once hardware advances again, color(display-p3 ...) will have the same problem as sRGB colors have today. LCH and Lab are device independent, and can represent the entire gamut of human vision so they will work regardless of how hardware advances.

How does LCH relate to the Lab color space that I know from Photoshop and other applications?

LCH is the same color space as Lab, just viewed differently! Take a look at the following diagram that I made for my students:

The L in Lab and LCH is exactly the same (perceptual Lightness). For a given lightness L, in Lab, a color has cartesian coordinates (L, a, b) and polar coordinates (L, C, H). Chroma is just the length of the line from 0 to point (a, b) and Hue is the angle of that ray. Therefore, the formulae to convert Lab to LCH are trivial one liners: C is sqrt(a² + b²) and H is atan(b/a) (with different handling if a = 0). atan() is just the reverse of tan(), i.e. tan(H) = b/a.

Articles, CSS WG, Original, Releases, Colors, CSS Color 4, CSS Color 5, LCH, Web Standards
Edit post on GitHub

Conical gradients, today!

2 min read 0 comments Report broken page

Screen Shot 2015-06-18 at 16.26.40It’s no secret that I like conical gradients. In as early as 2011, I wrote a draft for conical-gradient() in CSS, that Tab later said helped him when he added them in CSS Image Values Level 4 in 2012. However, almost three years later, no progress has been made in implementing them. Sure, the spec is still relatively incomplete, but that’s not the reason conical gradients have gotten no traction. Far more underspecified features have gotten experimental implementations in the past. The reason conical gradients are still unimplemented, is because very few developers know they exist, so browsers see no demand.

Another reason was that Cairo, the graphics library used in Chrome and Firefox had no way of drawing a conical gradient. However, this changed a while ago, when they supported mesh gradients, of which conical gradients are a mere special case.

Recently, I was giving a talk on creating pie charts with CSS on a few conferences, and yet again, I was reminded of how useful conical gradients can be. While every CSS or SVG solution is several lines of code with varying levels of hackiness, conical gradients can give us a pie chart with a straightforward, DRY, one liner. For example, this is how to create a pie chart that shows 40% in gold and 60% in #f06:

padding: 5em; /* size */
background: conic-gradient(gold 40%, #f06 0);
border-radius: 50%; /* make it round */

Screen Shot 2015-06-18 at 16.23.57 So, I decided to take matters in my own hands. I wrote a polyfill, which I also used in my talk to demonstrate how awesome conical gradients can be and what cool things they can do. Today, during my CSSConf talk, I released it publicly.

In addition, I mention to developers how important speaking up is for getting their favorite features implemented. Browsers prioritize which features to implement based on what developers ask for. It’s a pity that so few of us realize how much of a say we collectively have in this. This is more obvious with Microsoft and their Uservoice forum where developers can vote on which features they want to see worked on, but pretty much every major browser works in a similar way. They monitor what developers request and what other browsers implement, and decide accordingly. The squeaky wheel will get the feature, so if you really want to see something implemented, speak up.

Since “speaking up” can be a bit vague (“speak up where?” I can hear you asking), I also filed bug reports with all major browsers, that you can also find in the polyfill page, so that you can comment or vote on them. That doesn’t mean that speaking up on blogs or social media is not useful though: That’s why browsers have devrel teams. The more noise we collectively make about the features we want, the more likely it is to be heard. However, the odds are higher if we all channel our voices to the venues browser developers follow and our voice is stronger and louder if we concentrate it in the same places instead of having many separate voices all over the place.

Also, I’m using the term “noise” here a bit figuratively. While it’s valuable to make it clear that we are interested in a certain feature, it’s even more useful to say why. Providing use cases will not only grab browsers’ attention more, but it will also convince other developers as well.

So go ahead, play with conic gradients, and if you agree with me that they are fucking awesome and we need them natively on the Web, make noise.

conic-gradient() polyfill


An easy notation for grayscale colors

2 min read 0 comments Report broken page

These days, there is a lengthy discussion in the CSS WG about how to name a function that produces shades of gray (from white to black) with varying degrees of transparency, and we need your feedback about which name is easier to use.

The current proposals are:

1. gray(lightness [, alpha])

In this proposal gray(0%) is black, gray(50%) is gray and gray(100%) is white. It also accepts numbers from 0-255 which correspond to rgb(x,x,x) values, so that gray(255) is white and gray(0) is black. It also accepts an optional second argument for alpha transparency, so that gray(0, .5) would be equivalent to rgba(0,0,0,.5).

This is the naming of the function in the current CSS Color Level 4 draft.

2. white(lightness [, alpha])

Its arguments work in the same way as gray(), but it’s consistent with the expectation that function names that accept percentages give the “full effect” at 100%. gray(100%) sounds like a shade of gray, when it’s actually white. white(100%) is white, which might be more consistent with author expectations. Of course, this also accepts alpha transparency, like all the proposals listed here.

3. black(lightness [, alpha])

black() would work in the opposite way: black(0%) would be white, black(100%) would be black and black(50%,.5) would be semi-transparent gray. The idea is that people are familiar thinking that way from grayscale printing.

4. rgb() with one argument and rgba() with two arguments

rgb(x) would be a shorthand to rgb(x, x, x) and rgba(x, y) would be a shorthand to rgba(x, x, x, y). So, rgb(0) would be black and rgb(100%) or rgb(255) would be white. The benefit is that authors are already accustomed to using rgb() for colors, and this would just be a shortcut. However, note how you will need to change the function name to get a semi-transparent version of the color. Also, if in the future one needs to change the color to not be a shade of gray, a function name change is not needed.

I’ve written some SCSS to emulate these functions so you can play with them in your stylesheets and figure out which one is more intuitive. Unfortunately rgb(x)/rgba(x,a) cannot be polyfilled in that way, as that would overwrite the native rgb()/rgba() functions. Which might be an argument against them, as being able to polyfill through a preprocessor is quite a benefit for a new color format IMO.

You can vote here, but that’s mainly for easy vote counting. It’s strongly encouraged that you also leave a comment justifying your opinion, either here or in the list.

Vote now!

Also tl;dr If you can’t be bothered to read the post and understand the proposals well, please, refrain from voting.