I've been slowly working on a side project for a few moths now. In the course of that I found I had a need to flip the text color of an element between white and black depending on the lightness of the background color. This is something I had wanted to do some years ago, but at the time there wasn't a way to do it. Given all the changes to CSS since then, I wondered if it could be done now.
I took to the internet as one does in these cases and to my great excitement, in short order, I was able to put together two features in CSS to do just this!
First, let be demonstrate what I'm trying to accomplish. Imagine we have a button as part of a design system (forgive the lack of aesthetics on my part). We have two variants of this button. One has a lighter background, which requires darker text.
<button is="proper-button">click to activate</button>
The other, secondary button, has a darker background that requires lighter text.
<button is="proper-button" secondary>click to delete</button>
See how the text color for the lighter background is black, and the text-color for the darker background is white? What if I told you both of these button use the same definition for color
?
Look. Here's an example to play with. Set the --color
CSS variable in the style attribute in this button to anything. Any color you want. Watch how the text color automatically changes to either white or black depending on the lightness of the background color.
<button
is="proper-button"
style="--color: black" /* <-- change this value*/
>Clickity clack</button>
How cool is that?!?! Let's look at what's going on here. Our color
property is defined like this:
color: oklch(from var(--color) calc(1 - round(l)) 0 h)
The Breakdown
There's a bit going on here, so let's break it down.
Color Spaces
First. we're using the oklch
color space. Any color space will mostly work, but I like oklch
because it's designed to represent percieved color, which comes in most handy as we want to toggle between black and white depending on the reference color's brightness.
oklch(...)
Relative color values
Next, we're using the relative values syntax. This is relatively new addtion to the CSS spec, but it is widely supported and is considered baseline. It allows us to define a color, based on some other color. The really cool thing is that the reference color does not need to have been defined in the same color space. The CSS engine will do the work of translating between color spaces for us. How nice!
oklch(from ...)
Of course, we need need a reference color. In our case, we're using a color defiend in a CSS variable, --color
. This is what allows our color
property definition to be so dynamic and agnostic to our reference color.
oklch(from var(--color) ...)
The next part has to do again with how relative values work in color definitions. We're able to take each component of our color definition, and manipulate it individually. For instance. This example manipulates the l
or lightness value of the color black
to be 50% -- making it gray
oklch(from black 50% c h);
That's neat and all, but it gets more interesting when we discover we can perform calculations on these values. So instead of setting it to a fixed value of 50%, let's change it to be an additional 50% as light as its original value.
oklch(from black calc(l + 50%) c h);
We can do the same thing for the c
or chroma component of our color. Chroma is the amount of color. Its value ranges between 0
and 0.4
which is a bit confusing, so I often prefer to use percentages rather than values in my calculations. We're already dealing with relative values, so percentages let's stay in that head space.
oklch(from black l calc(c + 50%) h);
Round-ing the corner
Now. Our goal hear was to toggle between a black text color when the background is "light" and a white text color when the background is "dark". Our next problem is to define what "light" and "dark" mean from a value perspective. Since our text color lives on the extremes of the light/dark spectrum -- black having a value of 0
and white having a value of 1
-- let's split the difference. If a background color has at least 50% lightness, we should have the text color be on the opposite extreme, so we go black. If it has less than 50%, we go the other way and use a white text color. Seems simple enough. Except, how do we know how much lightness a color has. We can do math on our color, but we can't use conditionals (at least not yet). If only there were some mathy thing we could do that would force a value to 0 if it were less than .5 or to 1 if it were at least .5 🤔.
Enter round()
. The CSS function round
does exactly what you think it does. round
will round a value to the nearest integer. You can give it some strategies, but the default is exactly what we want here. Less than half, round down. Half or greater, round up. We can use round
on our l
(lightness) value to go either fully light or fully dark depending on our needs. We'll also need to completely desaturate our c
(chroma) value in any case, so we can get that fully black & white experience.
color: oklch(from var(--color) calc(1 - round(l)) 0 h)
And there we have it. How (and why) to flip between back and white text color depending on the lightness of the background color. Just don't use transparent
😳.
<button
is="proper-button"
style="
--color: black;
background-color: var(--color);
color: oklch(from var(--color) calc(1 - round(l)) 0 h);
"
>Clickity clack</button>