Firefox 146 added support for contrast-color() joining Safari 26 in the First Implementor’s Club. For those unfamiliar, contrast-color(<color>) is a new CSS function that will take a <color> as input and returns either white or black depending on which has the most contrast.
The quintessential example is choosing a foreground text color with the best contrast.
button {
--button-bg: red;
background: var(--button-bg);
color: contrast-color(var(--button-bg));
/* @returns black (5.25:1 WCAG AA Pass)
not white (3.99:1 WCAG AA Fail) */
}
If someone changes --button-bg to purple, the foreground color automatically resolves to the either white or black, whichever has more contrast. It avoids having to set an extra token for color and takes the guess work out of picking an accessible foreground color.
I think this is going to be an incredible boon to design systems where I don’t control what --button-bg is, but I do care about providing accessible experiences. And I think that’s the goal of this feature; to have “smart defaults” that lead to more accessible websites, easier algorithmically-driven color systems, and better “theme a whole website from a single color picker” demos.
Sure contrast-color() can do foreground colors, but what about backgrounds?
We’re having conversations at work about algorithmically driven rest/hover/active states for Buttons. In the current Baseline you can use color-mix() to lighten/darken colors on :hover…
button {
background-color: var(--button-bg);
color: contrast-color(var(--button-bg));
&:hover, &:focus {
background-color: color-mix(
in srgb,
var(--button-bg) 90%,
black 10%
)
}
}
The code above will dim your button on :hover 10% by mixing in black. But what if our Buttons are already dark (black, navy, etc)? In that situation we want to lighten the background-color instead of dimming. We can glue on new classes like button.lighten-on-hover or button.invert-hover and that works… until we get to light and dark theme modes of our Button where you probably want to lighten/darken oppositely depending on the mode…
Ugh. In that situation you’d have @media (prefers-color-scheme: dark), [data-theme="dark"] styles in your Button styles and people are mad now because the Button styles are too complex. There’s got to be a better way!
Well, do I have good news for you…
See the Pen contrast-color() powered lighten/darken bg on hover by Dave Rupert (@davatron5000) on CodePen.
Per my previous conversations, I wondered if we could use contrast-color() to programmatically lighten/darken an button based on its current background-color. If the Button is black, and the contrast-color is white, let’s mix in white (and vice versa):
:root {
color-scheme: light dark;
--button-bg: light-dark(navyblue, lightpurple);
}
button {
background-color: var(--button-bg);
color: color-contrast(var(--button-bg));
&:hover, &:focus {
background-color: color-mix(
in srgb,
var(--button-bg) 75%,
contrast-color(var(--button-bg)) 10%
)
}
}
Huzzah! Now our Button’s hover states go in the desired direction and our foreground color is intrinsically styled based on its own background-color. Nice. From a design systems perspective I’m pretty excited about the possibility to remove a bunch of state-based tokens from our collection.
Now available in… everywhere?
This approach only works in Safari and Firefox. However, if I use Lea Verou’s method of polyfilling contrast-color(), we can pop in a custom @function --contrast-color() that works in Chromium 139+. The final working solution looks like this:
/* @function supported in Chromium */
@function --contrast-color(--bg-color) {
--l: clamp(0, (l / var(--l-threshold, 0.623) - 1) * -infinity, 1);
result: oklch(from var(--bg-color) var(--l) 0 h);
}
button {
/* contrast-color() supported in Safari & Firefox */
--button-fg: contrast-color(var(--button-bg));
@supports not (color: contrast-color(red)) {
--button-fg: --contrast-color(var(--button-bg));
}
background: var(--button-bg);
color: var(--button-fg);
&:hover,
&:focus{
background-color: color-mix(
in srgb,
var(--button-bg) 75%,
var(--button-fg) 10%
);
}
}
Depending on your browser matrix, this may work for you. It’s probably a smidge too new for us to roll out to customers, right now but for personal sites heck yeah.
This is interesting tech and I’m excited to dig in more. And spoiler alert, this is the first post in a small little series I have already written up for you.