Skip to content

Web Development · CSS

CSS contrast-color() Is Baseline: Stop Hardcoding

Chrome 147, Firefox 146, and Safari 26 all ship contrast-color(), which picks black or white text to meet WCAG AA on any background. Syntax and fallback.

Abhishek Gupta

Abhishek Gupta

4 min read

contrast-color() Is Now Baseline

Sponsored

Share

Pick a background color at build time and you can hardcode the text color that reads well against it. Let a user pick their own theme color, pull a brand color from a CMS field, or generate a chart legend from data, and you’re stuck computing luminance in JavaScript just to decide between black and white text. As of April 2026, you don’t have to. contrast-color() shipped in Chrome 147, Firefox 146, and Safari 26.0, reached Baseline Newly Available the same month, and does that calculation natively in CSS.

What it does

contrast-color() takes one color and returns whichever of black or white contrasts better against it, targeting the WCAG 2 AA minimum of 4.5:1 for normal-size text:

.badge {
  --bg: #2d5a27;
  background-color: var(--bg);
  color: contrast-color(var(--bg));
}

Dark forest green in, white out. Change --bg to a pale yellow and the same declaration returns black, with zero JavaScript and no re-render triggered by anything other than the browser’s normal style recalculation. That’s the entire pitch: a computation that used to require running code on every theme change now runs as part of computing styles, exactly like currentColor or any other CSS value function.

Where it actually earns its keep

The function is genuinely useful in exactly one situation: text color needs to react to a background color you don’t control at author time. A few concrete cases:

  • User-generated or CMS-driven brand colors. A multi-tenant dashboard where each customer picks their own accent color, or a CMS field that lets a marketer type in a hex value, both need the accompanying text to stay readable without a human checking every possible input.
  • Chart and data visualization legends. Bar and pie chart segments get colors assigned programmatically from a palette or a data-driven scale. Label text sitting on top of those segments needs the same automatic contrast decision.
  • Avatar initials and status badges. Generate an avatar background color by hashing a username, and the initials rendered on top need to stay legible across the entire hash space, not just the colors someone happened to test.
  • Theme-able component libraries. A design system that ships light and dark variants, plus arbitrary brand-color overrides, gets one contrast rule instead of a color-mode-and-brand-color lookup table.

If your background colors are fixed at design time, a hardcoded color: white is simpler, faster to reason about, and doesn’t need this function at all. Save it for the cases where the background genuinely varies at runtime.

The fallback pattern

Baseline Newly Available means it’s safe to ship, not that every visitor’s browser is new enough to have it. Wrap it in @supports so anyone on an older engine gets a sane static color instead of an unstyled or illegible one:

.badge {
  --bg: #2d5a27;
  background-color: var(--bg);
  color: white; /* fallback for browsers before April 2026 */
}

@supports (color: contrast-color(red)) {
  .badge {
    color: contrast-color(var(--bg));
  }
}

@supports degrades gracefully: browsers that don’t recognize contrast-color() skip the block entirely and keep the fallback declaration. This is the same pattern worth using for any newly Baseline feature while you wait out the tail of your traffic on older engines, similar to how CSS anchor positioning needs a fallback layer for tooltips and popovers on browsers that predate it.

What it doesn’t do yet

The version that shipped is intentionally narrow: one input color, two possible outputs, black or white, targeting a fixed AA ratio. There’s no way to pick from a brand palette, no way to ask for AAA instead of AA, and no way to pass a list of acceptable colors and let the browser choose the best match.

A CSS Color Level 6 draft addresses all three: contrast-color(var(--bg) vs #1a1a2e, #e2e8f0, #fbbf24) would let you supply your own candidate colors instead of being locked to pure black or white, plus an optional target parameter (aa, aaa, aa-large, aaa-large, or a raw number) instead of the fixed default. That syntax exists only as a Working Draft right now, with no browser shipping it. Build against the single-argument version that’s actually in Chrome, Firefox, and Safari today, and treat the extended syntax as a “watch this” rather than something to plan around.

The takeaway

contrast-color() replaces a narrow but common piece of JavaScript, deciding between black and white text against a runtime-determined background, with a one-line CSS declaration that’s now supported across all three major engines. It’s not a general-purpose color-picking tool, and it won’t replace your design system’s palette. For the specific problem of “this background color isn’t known until runtime, and I need legible text on it,” it’s the correct tool as of this year, provided you keep the @supports fallback in place for the browsers that came before April 2026.

Frequently asked questions

What does contrast-color() actually return?
Either pure black or pure white, whichever gives a higher contrast ratio against the color you pass in. The browser computes the WCAG contrast ratio of both black and white against your input color and picks the winner, targeting the WCAG 2 AA minimum of 4.5:1 for normal-size text. If both options happen to tie, the specification defaults to white.
Which browsers support contrast-color() right now?
All three major engines ship it in stable releases as of April 2026: Chrome 147, Firefox 146, and Safari 26.0. That combination is what earned it Baseline Newly Available status. If your analytics show meaningful traffic on older browser versions, wrap the declaration in an @supports check with a static fallback color.
Can I pick from more than black and white?
Not with the syntax that's actually shipped. A CSS Color Level 6 proposal adds a candidate-color-list form, contrast-color(bg-color vs color1, color2, color3), where the browser walks the list and picks the first one meeting a target ratio you specify (AA, AAA, or a numeric value). That extended syntax is still a Working Draft with no shipping implementation, so build against the single-argument black-or-white version today.
Does this replace JavaScript color-contrast utilities?
For the common case, computing readable text color against a background you don't control at author time, yes, once your browser support floor allows it. Libraries like tinycolor2's readability helpers or hand-rolled luminance calculations exist specifically to solve this in JavaScript, running on every render or every theme change. contrast-color() moves that computation into the browser's rendering pipeline, evaluated declaratively alongside the rest of your styles, with no re-render needed when the background color itself changes via a CSS custom property.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored