Swap between black and white text based on background color
The title is overselling it a little, but not by much. Devon Govett posted this clever trick on Bluesky using CSS relative colors and LCH:
.magic {
  --bg: red;
  background: var(--bg);
  color: lch(from var(--bg) calc((49.44 - l) * infinity) 0 0);
}So it won’t automagically work with any color behind the element; you need to have it stored as a CSS variable.
LCH has three properties: lightness, chroma and hue.
- Lightness is a number between 0and100representing how bright a color is.0corresponds to black, while100corresponds to white.
- Chroma is a technically unbounded number that represents “how much” color is present.
- Hue is an angle that represents the hue angle, as in HSL or HSV.
You supply them to the lch function like this:
.magic {
  --lightness: 50;
  --chroma: 72.2;
  --hue: 56.2;
  color: lch(var(--lightness) var(--chroma) var(--hue));
}When used with relative color syntax, the color gets “broken up” into its constituent parts which are referred to by l, c and h. So, for example, this would set the background color to the same as the text color; l, c and h take their values from the from color and are placed in the appropriate slots unmodified.
.magic {
  --bg: red;
  background: var(--bg);
  color: lch(from var(--bg) l c h);
}Devon’s example makes two big changes:
- It discards the chroma and hue values, replacing them with 0.
- It inverts the color’s lightness and multiplies it by infinityto obtain white or black.
#2 might be confusing, so let’s dig into some examples. The basic idea is that if a color’s lightness is above some threshold value, we want the text to be black; if it’s below that value, we want the text to be white.
Remember, the calculation is (49.44 - l) * infinity, clamped within the range [0, 100]:
- CSS redhas an LCH lightness of54.29.- 49.44-- 54.29=- -4.85
- -4.85*- infinity=- -infinity
- -infinitygets clamped to- 0(black)
 
- CSS bluehas an LCH lightness of29.57.- 49.44-- 29.57=- 19.87
- 19.87*- infinity=- -infinity
- infinitygets clamped to- 100(white)
 
- CSS whitehas an LCH lightness of100.- 49.44-- 100=- -50.56
- -4.85*- infinity=- -infinity
- -infinitygets clamped to- 0(black)
 
Why 49.44? Devon tested it with all RGB colors and found it had the least number of WCAG 4.5:1 contrast failures.
Addendum: After publishing, Noah Liebman pointed out to me that Lea Verou had independently come up with the same technique earlier this year!