- The Problem We’ve All Been Living With
- The Syntax (It’s Embarrassingly Simple)
- Real-World Use Cases
- Dynamic Theming
- Dark Mode, Finally Painless
- User-Customizable UI
- The Gotcha You Should Know About
- Fallbacks with @supports
- So… Game Changer or Nice to Have?
This morning, on the train, half-awake, coffee not yet fully processed — my phone buzzed. A new entry in my RSS feed from the Web Platform Status Explorer. I subscribe to that feed because it’s genuinely one of the best ways to catch when browser features cross the line from “available somewhere” to baseline widely available.
I opened it. Read the feature name. Read it again.
contrast-color()
I actually sat up straighter in my seat. Because this is one of those things. One of those problems that’s been quietly annoying me for years — something you solve with JavaScript, or with a SCSS function, or with a carefully maintained lookup table of color pairs — and it just… became a native CSS function. Overnight. Supported everywhere. Done.
That’s the kind of notification that makes you want to write a blog post from the train.
So here we are.
The Problem We’ve All Been Living With
You know the drill. You’ve got a design system, a theme, or maybe just a simple button. Someone changes the background color — maybe it’s user-defined, maybe it’s a dark mode toggle, maybe it’s coming from a CMS — and suddenly your text is dark gray on dark gray, barely readable, technically accessible-ish if you squint.
The classic solution? Maintain a list of color pairs. Background A goes with text color X. Background B goes with text color Y. And when a new color gets added? Someone updates a JavaScript utility. Or forgets to. Usually forgets to.
We’ve been solving this with Chroma.js, with luminance() calculations, with prefers-color-scheme media queries juggling CSS variables. It works — but it’s ceremony. Boilerplate for a problem that feels like it should just be solved by the browser.
And now it is.
Enter: contrast-color()
contrast-color() is a new CSS function that takes any valid color value and automatically returns either black or white — whichever one has the better contrast ratio against the input color.
That’s it. That’s the feature. And it’s kind of perfect.
No JavaScript. No utility function. No maintaining pairs. The browser does the math using the WCAG AA contrast algorithm (4.5:1), and you get a readable color back.
.button {
background-color: var(--brand-color);
color: contrast-color(var(--brand-color));
}
One line. Your text is always readable against your background. Welcome to the future.
The Syntax (It’s Embarrassingly Simple)
/* With a named color */
color: contrast-color(navy);
/* With a hex value */
color: contrast-color(#ff6600);
/* With a CSS custom property — the real magic */
color: contrast-color(var(--background-color));
/* With any valid color function */
color: contrast-color(oklch(70% 0.2 150));
The return value is always either white or black. No gradients, no intermediate grays — just the right one. If both happen to have identical contrast (rare, but possible), white wins.
This pairs incredibly well with CSS custom properties. If you’re already running a design system with CSS variables for your colors, you can slot contrast-color() in right now and get automatic text contrast across the board.
Real-World Use Cases
Dynamic Theming
Imagine a theme generator — the kind of thing where users pick a brand color and everything updates live. Previously you’d have a JavaScript listener recalculating text colors whenever the color changed. Now:
:root {
--brand: #2563eb;
}
.badge {
background-color: var(--brand);
color: contrast-color(var(--brand));
}
Update --brand via JavaScript, and the text color follows automatically. No extra logic needed.
Dark Mode, Finally Painless
:root {
--surface: #ffffff;
}
@media (prefers-color-scheme: dark) {
:root {
--surface: #1a1a2e;
}
}
body {
background-color: var(--surface);
color: contrast-color(var(--surface));
}
One variable, one contrast-color() call. Your light and dark modes are handled. It’s almost unfair.
User-Customizable UI
Think color pickers, tags, labels, avatars — any place where the color comes from user input or an external data source:
.tag {
background-color: var(--tag-color);
color: contrast-color(var(--tag-color));
border-radius: 999px;
padding: 0.25rem 0.75rem;
}
However --tag-color ends up being set — from an inline style, from a JS property, from a CMS — the text will be readable. Every time.
The Gotcha You Should Know About
brakes a little, because there is a limitation worth knowing.
contrast-color() only returns black or white. This means that for mid-tone backgrounds — think #888888, or a royal-ish blue like #2277d3 — neither black nor white might actually be great. The function will pick the less-bad option, but if your background sits in that awkward middle zone, you might end up with technically-acceptable but visually uncomfortable text.
The MDN docs call this out explicitly: WCAG AA is not capable of producing clearly readable text in all cases. Mid-tones are the pain point.
The practical takeaway? contrast-color() shines with clearly light or clearly dark backgrounds. For anything in the middle — especially if you care about aesthetics as much as accessibility — consider restricting your color choices at the design level, or using contrast-color() as a safety net rather than a primary solution.
Fallbacks with @supports
It’s baseline widely available now, which means you can use it in production without too much worry. But if you want to be safe for any lingering holdouts, @supports has your back:
/* Fallback for browsers that don't support contrast-color() */
.button {
background-color: var(--brand-color);
color: white; /* Sensible default */
}
/* Progressive enhancement */
@supports (color: contrast-color(red)) {
.button {
color: contrast-color(var(--brand-color));
}
}
Clean, minimal, works everywhere.
So… Game Changer or Nice to Have?
Honestly? Both, depending on your project.
If you’re building anything with dynamic colors — themes, user customization, CMSdriven designs — contrast-color() is a genuine game changer. It eliminates a whole category of boilerplate and makes your CSS smarter without reaching for JavaScript.
If you’re working on a tightly designed product with a fixed, curated palette, it’s a nice safety net. Not transformative, but definitely a tool worth having.
What I’m most excited about is what it enables architecturally. A CSS-first design system where color contrast is just… handled. Where you don’t have to think about it anymore because the browser has your back. That’s the direction CSS has been heading — more power, less ceremony — and contrast-color() fits right into that story.
Now go refactor that color utility. You know the one. 🚀
