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
0
and100
representing how bright a color is.0
corresponds to black, while100
corresponds 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
infinity
to 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
red
has an LCH lightness of54.29
.49.44
-54.29
=-4.85
-4.85
*infinity
=-infinity
-infintiy
gets clamped to0
(black)
- CSS
blue
has an LCH lightness of29.57
.49.44
-29.57
=19.87
19.87
*infinity
=-infinity
infintiy
gets clamped to100
(white)
- CSS
white
has an LCH lightness of100
.49.44
-100
=-50.56
-4.85
*infinity
=-infinity
-infintiy
gets clamped to0
(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!