Exploring the CSS contrast-color() Function⦠a Second Time
In many countries, web accessibility is a human right and the law, and there can be heavy fines for non-compliance. Naturally, this means that text and icons and such must have optimal color contrast in accordance with the benchmarks set by the Web Content Accessibility Guidelines (WCAG). Now, there are quite a few color contrast checkers out there (Figma even has one built-in now), but the upcoming contrast-color() function doesnât check color contrast, it outright resolves to either black or white (whichever one contrasts the most with your chosen color).
Right off the bat, you should know that weâve sorta looked at this feature before. Back then, however, it was called color-contrast() instead of contrast-color() and had a much more convoluted way of going about things. It was only released in Safari Technology Preview 122 back in 2021, and thatâs still the case at the time Iâm writing this (now at version 220).
Youâd use it like this:
button {
--background-color: darkblue;
background-color: var(--background-color);
color: contrast-color(var(--background-color));
}
Here, contrast-color() has determined that white contrasts with darkblue better than black does, which is why contrast-color() resolves to white. Pretty simple, really, but there are a few shortcomings, which includes a lack of browser support (again, itâs only in Safari Technology Preview at the moment).
We can use contrast-color() conditionally, though:
@supports (color: contrast-color(red)) {
/* contrast-color() supported */
}
@supports not (color: contrast-color(red)) {
/* contrast-color() not supported */
}
The shortcomings of contrast-color()
First, let me just say that improvements are already being considered, so here Iâll explain the shortcomings as well as any improvements that Iâve heard about.
Undoubtedly, the number one shortcoming is that contrast-color() only resolves to either black or white. If you donât want black or white, well⦠that sucks. However, the draft spec itself alludes to more control over the resolved color in the future.
But thereâs one other thing thatâs surprisingly easy to overlook. What happens when neither black nor white is actually accessible against the chosen color? Thatâs right, itâs possible for contrast-color() to just⦠not provide a contrasting color. Ideally, I think weâd want contrast-color() to resolve to the closest accessible variant of a preferred color. Until then, contrast-color() isnât really usable.
Another shortcoming of contrast-color() is that it only accepts arguments of the <color> data type, so itâs just not going to work with images or anything like that. I did, however, manage to make it âworkâ with a gradient (basically, two instances of contrast-color() for two color stops/one linear gradient):
<button>
<span>A button</span>
</button>
button {
background: linear-gradient(to right, red, blue);
span {
background: linear-gradient(to right, contrast-color(red), contrast-color(blue));
color: transparent;
background-clip: text;
}
}
The reason this looks so horrid is that, as mentioned before, contrast-color() only resolves to black or white, so in the middle of the gradient we essentially have 50% grey on purple. This problem would also get solved by contrast-color() resolving to a wider spectrum of colors.
But what about the font size? As you might know already, the criteria for color contrast depends on the font size, so how does that work? Well, at the moment it doesnât, but I think itâs safe to assume that itâll eventually take the font-size into account when determining the resolved color. Which brings us to APCA.
APCA (Accessible Perceptual Contrast Algorithm) is a new algorithm for measuring color contrast reliably. Andrew Somers, creator of APCA, conducted studies (alongside many other independent studies) and learned that 23% of WCAG 2 “Fails” are actually accessible. In addition, an insane 47% of “Passes” are inaccessible.
Not only should APCA do a better job, but the APCA Readability Criterion (ARC) is far more nuanced, taking into account a much wider spectrum of font sizes and weights (hooray for me, as Iâm very partial to 600 as a standard font weight). While the criterion is expectedly complex and unnecessarily confusing, the APCA Contrast Calculator does a decent-enough job of explaining how it all works visually, for now.
contrast-color() doesnât use APCA, but the draft spec does allude to offering more algorithms in the future. This wording is odd as it suggests that weâll be able to choose between the APCA and WCAG algorithms. Then again, we have to remember that the laws of some countries will require WCAG 2 compliance while others require WCAG 3 compliance (when it becomes a standard).
Thatâs right, weâre a long way off of APCA becoming a part of WCAG 3, let alone contrast-color(). In fact, it might not even be a part of it initially (or at all), and there are many more hurdles after that, but hopefully this sheds some light on the whole thing. For now, contrast-color() is using WCAG 2 only.
Using contrast-color()
Hereâs a simple example (the same one from earlier) of a darkblue-colored button with accessibly-colored text chosen by contrast-color(). Iâve put this darkblue color into a CSS variable so that we can define it once but reference it as many times as is necessary (which is just twice for now).
button {
--background-color: darkblue;
background-color: var(--background-color);
/* Resolves to white */
color: contrast-color(var(--background-color));
}
And the same thing but with lightblue:
button {
--background-color: lightblue;
background-color: var(--background-color);
/* Resolves to black */
color: contrast-color(var(--background-color));
}
First of all, we can absolutely switch this up and use contrast-color() on the background-color property instead (or in-place of any <color>, in fact, like on a border):
button {
--color: darkblue;
color: var(--color);
/* Resolves to white */
background-color: contrast-color(var(--color));
}
Any valid <color> will work (named, HEX, RGB, HSL, HWB, etc.):
button {
/* HSL this time */
--background-color: hsl(0 0% 0%);
background-color: var(--background-color);
/* Resolves to white */
color: contrast-color(var(--background-color));
}
Need to change the base color on the fly (e.g., on hover)? Easy:
button {
--background-color: hsl(0 0% 0%);
background-color: var(--background-color);
/* Starts off white, becomes black on hover */
color: contrast-color(var(--background-color));
&:hover {
/* 50% lighter */
--background-color: hsl(0 0% 50%);
}
}
Similarly, we could use contrast-color() with the light-dark() function to ensure accessible color contrast across light and dark modes:
:root {
/* Dark mode if checked */
&:has(input[type="checkbox"]:checked) {
color-scheme: dark;
}
/* Light mode if not checked */
&:not(:has(input[type="checkbox"]:checked)) {
color-scheme: light;
}
body {
/* Different background for each mode */
background: light-dark(hsl(0 0% 50%), hsl(0 0% 0%));
/* Different contrasted color for each mode */
color: light-dark(contrast-color(hsl(0 0% 50%)), contrast-color(hsl(0 0% 0%));
}
}
The interesting thing about APCA is that it accounts for the discrepancies between light mode and dark mode contrast, whereas the current WCAG algorithm often evaluates dark mode contrast inaccurately. This one nuance of many is why we need not only a new color contrast algorithm but also the contrast-color() CSS function to handle all of these nuances (font size, font weight, etc.) for us.
This doesnât mean that contrast-color() has to ensure accessibility at the expense of our âdesignedâ colors, though. Instead, we can use contrast-color() within the prefers-contrast: more media query only:
button {
--background-color: hsl(270 100% 50%);
background-color: var(--background-color);
/* Almost white (WCAG AA: Fail) */
color: hsl(270 100% 90%);
@media (prefers-contrast: more) {
/* Resolves to white (WCAG AA: Pass) */
color: contrast-color(var(--background-color));
}
}
Personally, Iâm not keen on prefers-contrast: more as a progressive enhancement. Great color contrast benefits everyone, and besides, we canât be sure that those who need more contrast are actually set up for it. Perhaps theyâre using a brand new computer, or they just donât know how to customize accessibility settings.
Closing thoughts
So, contrast-color() obviously isnât useful in its current form as it only resolves to black or white, which might not be accessible. However, if it were improved to resolve to a wider spectrum of colors, thatâd be awesome. Even better, if it were to upgrade colors to a certain standard (e.g., WCAG AA) if they donât already meet it, but let them be if they do. Sort of like a failsafe approach? This means that web browsers would have to take the font size, font weight, element, and so on into account.
To throw another option out there, thereâs also the approach that Windows takes for its High Contrast Mode. This mode triggers web browsers to overwrite colors using the forced-colors: active media query, which we can also use to make further customizations. However, this effect is quite extreme (even though we can opt out of it using the forced-colors-adjust CSS property and use our own colors instead) and macOSâs version of the feature doesnât extend to the web.
I think that forced colors is an incredible idea as long as users can set their contrast preferences when they set up their computer or browser (the browser would be more enforceable), and there are a wider range of contrast options. And then if you, as a designer or developer, donât like the enforced colors, then you have the option to meet accessibility standards so that they donât get enforced. In my opinion, this approach is the most user-friendly and the most developer-friendly (assuming that you care about accessibility). For complete flexibility, there could be a CSS property for opting out, or something. Just color contrast by default, but you can keep the colors youâve chosen as long as theyâre accessible.
What do you think? Is contrast-color() the right approach, or should the user agent bear some or all of the responsibility? Or perhaps youâre happy for color contrast to be considered manually?

