The HTTP Archive Web Almanac has been tracking color contrast failures for years. The numbers have barely moved. After half a decade of design system tooling, accessibility linters, and entire JavaScript libraries dedicated to computing readable text colors, 70% of websites still fail basic WCAG contrast checks in 2025. The WebAIM Million paints an even grimmer picture — 83.9% of homepages flagged for low contrast text in 2026, up from 79.1% in 2025. The rate improves by maybe a few percentage points per year on one benchmark and actually gets worse on another. That’s not progress — that’s proof that relying on runtime JavaScript for something this fundamental doesn’t scale across the open web. We didn’t need better libraries. We’ve needed better CSS.

The contrast-color() function is that better CSS. One declaration. The browser runs the contrast math during style computation, before the page paints, and hands you the right text color. No library, no build step, no hydration flash.

Note: If you’ve seen it called color-contrast() in older articles and spec drafts — that name was changed, and the old syntax no longer works in any browser.

What It Does (And What It Doesn’t)

The Level 5 version is simple. You give it a color. It gives you back black or white, whichever has more contrast against your input.

.button {
  background-color: var(--brand-color);
  color: contrast-color(var(--brand-color));
}

Change --brand-color to neon green, text goes black. Change it to midnight navy, text goes white. Swap themes at runtime via JavaScript and the text adapts instantly — no event listeners, no recalculation.

A few things to know about the current version:

  • It returns a <color>, not a number. You get an actual color value (black or white) you can use anywhere CSS accepts a color.
  • Black or white only, for now. Candidate color lists and target ratios are planned for Level 6.
  • No keywords. If you’ve seen max in older blog posts, that was stripped from the spec. Using it will silently break your declaration.
  • This function used to be called color-contrast() in early drafts. That name is dead — the CSSWG renamed it to follow the convention that CSS functions are named for what they return. color-mix() returns a color. contrast-color() returns a color. The old color-contrast() name sounded like it returned a contrast ratio (a number like 4.5), which was misleading. Any tutorial from 2021–2023 showing color-contrast() syntax won’t work in current browsers.

The Spec Split: Level 5 Versus Level 6

This function lives across two specifications. That’s unusual and worth understanding.

CSS Color Level 5 defines what browsers ship today. One color in, black or white out. The algorithm is deliberately marked “UA-defined”, meaning the browser decides what math to use internally. Right now, every engine uses WCAG 2.x relative luminance. But that “UA-defined” label isn’t accidental — it’s a planned escape hatch.

You’ll see APCA (Accessible Perceptual Contrast Algorithm) mentioned a lot in this context. APCA models how human eyes actually perceive contrast, factoring in font weight, spatial frequency, and ambient light — a genuine improvement over the WCAG 2.x formula. By not locking “use WCAG 2.x” into the Level 5 spec, browser vendors could swap to APCA later without breaking any existing code. If the spec had shipped with a wcag2() keyword as the default, every site using it would’ve been stuck on the old math permanently.

But APCA’s future is far less certain than the hype suggests. Adrian Roselli’s “WCAG3 Contrast as of April 2026” lays out the current situation clearly: APCA was pulled from the WCAG 3 working draft in mid-2023 after failing to gain enough Working Group support. The WCAG 3 spec currently says the contrast algorithm is “yet to be determined,” and the standard itself may not be finalized until 2030 or later. Roselli also filed a Chromium issue in May 2024 asking for the “Advanced Perceptual Contrast Algorithm” experiment flag to be removed from DevTools entirely, arguing that the implementation is outdated and risks misleading developers into thinking APCA is further along — or more official — than it actually is. That issue is still open.

None of this means APCA is dead. The research behind it is peer-reviewed and substantive, and its creator has noted that colors passing APCA guidelines greatly exceed WCAG 2 minimums in the vast majority of cases. But right now, there is no guarantee APCA will be the algorithm that replaces WCAG 2.x — and that uncertainty matters for contrast-color(). If a different algorithm wins out, or if WCAG 3 adopts something entirely new, the “UA-defined” label means browsers can adapt without breaking your code. It also means the Level 6 features — candidate color lists, target ratios, the tbd-fg/tbd-bg keywords — are all designed around an algorithm that may or may not materialize in its current form.

CSS Color Level 6 adds the extended syntax — candidate color lists and target contrast ratios:

/* Level 6 future syntax — not shipping yet */
color: contrast-color(var(--bg) tbd-bg wcag2(aa), #1a1a2e, #e2e8f0, #fbbf24);

The browser would evaluate each candidate left to right and pick the first that meets the 4.5:1 AA threshold. The tbd-fg and tbd-bg keywords indicate whether the base color is foreground or background, which matters for directional contrast models like APCA. This is all Working Draft territory — doubly so given APCA’s uncertain status. Use the Level 5 version for now.

Browser Support

This one’s in better shape than most new CSS features. All three major engines have shipped it in stable releases: Chrome 147 (April 2026), Firefox 146, and Safari 26.0. It reached Baseline Newly Available status in April 2026. Check caniuse for the full version matrix. All three engines pass the Web Platform Tests for contrast-color(), which means the edge cases (e.g., tie-breaking logic, color space conversion, syntax parsing) behave the same across browsers.

The raw global support percentage on caniuse looks low, but that mostly reflects enterprise browsers and people who never update. If you’re reading this, your browser almost certainly supports it already.

Progressive enhancement is straightforward using @supports:

.card {
  background: var(--bg);
  color: #fff;
  text-shadow: 0 0 4px rgb(0 0 0 / 0.8);
}

@supports (color: contrast-color(red)) {
  .card {
    color: contrast-color(var(--bg));
    text-shadow: none;
  }
}

Older browsers get white text with a dark shadow for legibility. Supporting browsers get the native calculation. Nobody sees broken text.

One thing to watch for: automated accessibility scanners (Lighthouse, Axe, etc.) can’t evaluate text-shadow. They only look at the computed color against background-color. So the fallback will still get flagged as a contrast failure in CI/CD pipelines, even if the shadow makes the text perfectly legible to human eyes. If your team runs automated a11y checks, you may need to allowlist that specific rule or add a comment explaining why the flag is a false positive.

A note on PostCSS: There’s a plugin (@csstools/postcss-contrast-color-function) that evaluates contrast-color() at build time. It works for static colors like contrast-color(#ff0000). But the moment you use a custom property — contrast-color(var(--bg)) — the plugin can’t help because it has no access to runtime values. If your theming is dynamic (which is the whole point of doing this), skip the polyfill and rely on @supports.

The Gotchas

It Doesn’t Guarantee Perceptual or AAA Compliance

This can trip people up: “I used the contrast function, so my site passes accessibility checks now, right?”

Mathematically? Usually yes. There is a persistent myth that for certain “mid-tone” backgrounds, both black and white fail the standard WCAG 4.5:1 AA ratio. That’s mathematically false. Under the WCAG 2.x relative luminance formula, there is absolutely no background color where both pure black and pure white fail AA. One (or both) will always pass.

Take #2277d3 (a medium blue). It sits right on a mathematical knife-edge where both black and white actually pass AA (both hit roughly 4.58:1). contrast-color() will hand you whichever has the slight mathematical edge.

But here is the actual gotcha: the WCAG 2.x math has known perceptual blind spots. That same #2277d3 with black text mathematically passes AA, but to human eyes, it can be incredibly difficult to read. contrast-color() gives you mathematical compliance, which is great for automated audits, but that doesn’t always equal perceptual accessibility. This is exactly why APCA exists and why the spec was designed to let browsers swap algorithms later.

Furthermore, if you’re aiming for the stricter WCAG AAA standard (7.0:1), a true dead zone does exist. For backgrounds with a luminance between roughly 10% and 30%, neither black nor white will hit 7:1. In those cases, contrast-color() still gives you the best available option — just not one that clears the AAA bar. If AAA compliance is a hard requirement for your project, you’ll need to constrain your background color palette to ranges where at least one of black or white can clear 7:1, and verify that separately from the function itself.