Educational guide to contrast-color()
Choosing text for a real background
Let CSS choose black or white for a background color. Learn to keep the pair in sync, recognize the limits with transparency and images, and check the contrast of what is painted.
Mental model
One color argument, two possible results, and an independent check.
- Reference color
The same token feeds background-color and contrast-color().
- Browser selection
Returns black or white; it does not inspect the page’s visual content.
- Check the result
Measure text against the composited background and review each state.
contrast-color(color) returns black or white. The browser defines the selection algorithm; you cannot supply a candidate palette, a target ratio, or a text size. The function returns a CSS color that we use here as a foreground.
Keeping foreground and background in sync
One shared variable avoids choosing text for a different color.
Define one opaque background color and also use it in color: contrast-color(var(--button-color)). When that variable changes, CSS resolves both values again. There is no need to move the text selection into a JavaScript listener.
The same rule on two opaque backgrounds
Background #660066 · dark
Fallback active: explicit color pair; contrast-color() is not applied.
Background #f5d0fe · light
Fallback active: explicit color pair; contrast-color() is not applied.
The first background uses the lab’s default state and shared declarations. The second only changes the color. The guide stays outside the controller: moving the playground picker does not change these samples.
Transparency and compositing
The same input does not describe two different final backgrounds.
A color with alpha lets lower layers contribute. contrast-color() receives a color, not the page’s composited background. These samples always pass rgb(255 255 255 / 0.2), but paint it over two different bases. The function does not receive that base as a second argument.
The same 20% white over white and soft black
Base #fff · white at 20%
Fallback active: explicit color pair; contrast-color() is not applied.
Base #111 · white at 20%
Fallback active: explicit color pair; contrast-color() is not applied.
For production text, use a known opaque surface or determine the composited color with all its layers and validate the resulting pair. Do not calculate contrast against the isolated transparent color’s RGB channels; changing the base, theme, or group opacity requires reviewing the result.
Images, gradients, and limits
One argument does not represent every pixel in an image.
The function accepts one color value, not an image or gradient. An average color or one sampled from a corner does not describe every area behind the text. This recipe places an opaque surface under the content and uses that surface as its reference.
Protecting text from a variable background
An opaque surface defines the real background of this text.
Fallback active: explicit color pair; contrast-color() is not applied.
Black or white also limits the visual direction: it does not return a brand color or check focus, borders, or icons for you. On mid-tone backgrounds, review size, weight, and actual legibility even if a measurement reaches its threshold. Choose a different surface if the text is difficult to read.
Checking contrast
Separate the browser’s selection from a WCAG 2.2 evaluation.
For normal text, WCAG 2.2 AA requires at least 4.5:1; for large text, 3:1. The large-text reference starts at 18 pt (24 CSS px) or 14 pt bold (approximately 18.67 CSS px). Evaluate the effective size and font; being a heading is not enough.
| Background | With black | With white |
|---|---|---|
#660066 | 1.76:1 Below 4.5:1 | 11.95:1 Reaches 4.5:1 |
#f5d0fe | 15.34:1 Reaches 4.5:1 | 1.37:1 Below 4.5:1 |
#777777 | 4.69:1 Reaches 4.5:1 | 4.48:1 Below 4.5:1 |
Ratios are calculated on the server using sRGB relative luminance. They are displayed with two decimal places, but the threshold is compared without rounding. #777777 illustrates a result near the minimum: there is no room to ignore transparency, states, or surface changes.
Ratio = (lighter luminance + 0.05) / (darker luminance + 0.05). This table only calculates opaque #RRGGBB pairs; it does not analyze pixels, images, alpha, or other color spaces.
- Get the computed text color and the final background behind it, including transparent layers. A computed background-color with alpha still needs compositing.
- Measure the pair in a contrast tool and repeat with hover, focus, selection, and each theme. With images, review the least favorable area or protect the text with a known surface.
- Also check keyboard use, visible focus, text enlargement, and forced colors. Visual information needed to identify controls and states has its own contrast requirement; choosing text does not resolve the whole interface.
A recipe with fallback and states
Keep known pairs when the function is unavailable.
The button has explicit background and text pairs for rest and hover/focus. @supports enables native selection; without it, the reviewed pairs still apply. The focus outline has its own color and space, and forced colors retain system values.
A button with two color states
Fallback active: explicit color pair; contrast-color() is not applied.
If you allow arbitrary colors, fixed fallbacks are no longer sufficient: constrain the palette, resolve and verify pairs on the server, or add logic that accounts for the real background. var(--color, fallback) does not validate a defined token; guard advanced functions with @supports and keep valid values.
Common mistakes
Review the input, layers, and final pair first.
Text chosen for another background
- Expected result
- The argument represents the surface behind the text.
- Cause
- A state changes background-color, but the function keeps another token.
- Solution
- Use a shared variable for both properties and review every state.
The result fails over transparency
- Expected result
- Sufficient contrast against the composited background.
- Cause
- The transparent input does not include lower layers or group opacity.
- Solution
- Set an opaque surface or calculate actual compositing before validating the pair.
Some text disappears over the image
- Expected result
- Readability across the entire text area.
- Cause
- One reference color does not describe every area of a gradient or photograph.
- Solution
- Place an opaque surface behind the text or verify the least favorable combination.
Support mistaken for accessibility
- Expected result
- Measured contrast and operable content.
- Cause
- A positive @supports result is treated as an interface audit.
- Solution
- Measure final colors and review size, focus, keyboard use, states, and preferences.
Support and compatibility
Function compatibility and example validity are separate checks.
In the October 14, 2026 review, MDN lists contrast-color() as Baseline Newly Available since April 2026. BCD records Chrome/Edge 147, Firefox 146, and Safari 26. Keep a fallback if your audience uses older versions.
The Baseline card queries the catalog status. @supports (color: contrast-color(#660066)) checks syntax in the current browser; it does not check your tokens or final contrast. The specification leaves the algorithm to the browser: a selection observed in one engine does not certify the others.
A shadow, text stroke, or isolated lightness threshold does not guarantee sufficient contrast. If you use another fallback strategy, check its result over the real background with the same criteria.