CSS contrast-color(): let the browser pick readable text

I thought I had seen every way a client could break a WordPress site. Then I shipped a block theme for one who wanted a color picker on every section background, and they picked a neon lime green that turned their white text invisible. The fix was a PHP filter that calculated luminance and cached the result in a transient, purely to keep the text readable. Ugly, but it held.

If the CSS contrast-color() function had existed then, it would have saved me about six hours of debugging. You hand it a color and it hands back black or white, whichever gives better contrast against it under the WCAG contrast requirements. The browser does the arithmetic instead of you.

How the syntax works

An earlier draft of the spec called this color-contrast() and let you pass a list of candidate colors to choose from. The W3C cut that back in Color Module Level 5, so the contrast-color() function you get today takes one color and returns one color. Fewer knobs, and nothing left to argue about in code review.

/* Basic usage with a variable */
.dynamic-card {
  background-color: var(--user-chosen-bg);
  /* contrast-color() returns white or black automatically */
  color: contrast-color(var(--user-chosen-bg));
}

/* Passing a hex value directly */
.alert-box {
  background-color: #34cdf2;
  color: contrast-color(#34cdf2);
}

If you have ever hit a race condition where the JavaScript color calculator loads after the first paint, you will like having this handled by the browser’s CSS engine instead. The luminance math leaves your JavaScript entirely.

Where it falls short

The function only returns black or white. If your design system calls for a specific dark grey rather than pure black, it cannot help you yet. It also ignores font-size and font-weight, both of which feed into the WCAG 2.1 thresholds.

  • You get black or white, so brand-specific contrasting colors are out.
  • It works on solid colors only. Text sitting over a hero image is still your problem.
  • Support is experimental, so you need a fallback before this goes anywhere near production.

I have written before about implementing CSS contrast color approximation with other methods, which holds up well enough while browser support fills in.

Shipping it safely with @supports

Support for contrast-color() is still landing in Safari and Firefox, so wrap it in a @supports rule and declare a sensible default color first.

.card {
  --bg: #2d5a27;
  background-color: var(--bg);
  
  /* Standard Fallback */
  color: #ffffff; 
}

@supports (color: contrast-color(red)) {
  .card {
    /* Use the function if the browser understands it */
    color: contrast-color(var(--bg));
  }
}

If this kind of CSS work is eating your dev hours, I can take it on. I have been building WordPress sites since the 4.x days.

What I would do with it today

The contrast-color() function moves a decision out of PHP and JavaScript and into the stylesheet, where it belongs. That matters most on block themes, where users can set any background they like and somebody still has to keep the text legible. Black and white is a narrow range for now, so add the function as an enhancement on your high-priority components and leave the fallback code exactly where it is.

author avatar
Ahmad Wael
I'm a WordPress and WooCommerce developer with 15+ years of experience building custom e-commerce solutions and plugins. I specialize in PHP development, following WordPress coding standards to deliver clean, maintainable code. Currently, I'm exploring AI and e-commerce by building multi-agent systems and SaaS products that integrate technologies like Google Gemini API with WordPress platforms, approaching every project with a commitment to performance, security, and exceptional user experience.