0Support

CSS contrast-color() playground

Explore contrast-color() with a background color picker. The example applies the selected color to a button and lets CSS automatically resolve whether the text should be black or white, with generated code inspired by MDN.

Read the guide

Ask AI

CSS contrast-color() playground

Response generated with openai/gpt-5.4-nano. AI can make mistakes. Always review the result.

Your browser does not support contrast-color() yet. A fallback is shown.

HTML
<button class="contrast-color-demo" type="button">
  contrast-color()
</button>
Tailwind CSS
Some classes with arbitrary values can be replaced by native classes, depending on the value and your Tailwind CSS configuration.
<button class="[--button-color:#660066] px-5 py-3 border-0 bg-(--button-color) text-[contrast-color(var(--button-color))]" type="button">
  contrast-color()
</button>
CSS
.contrast-color-demo {
  --button-color: #660066;

  padding: 0.75rem 1.25rem;
  border: 0;
  background-color: var(--button-color);
  color: contrast-color(var(--button-color));
}

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.

Back to playground

Mental model

One color argument, two possible results, and an independent check.

  1. Reference color

    The same token feeds background-color and contrast-color().

  2. Browser selection

    Returns black or white; it does not inspect the page’s visual content.

  3. Check the result

    Measure text against the composited background and review each state.

Static conceptual diagramThe argument should represent the surface behind the text. Accepting the syntax does not prove accessibility. The following samples apply the native function.

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 Aa samples use the native function when available. Without support, they keep an explicit fallback pair and display a notice. Outside labels identify each background in both themes.
The same rule on two opaque backgrounds
<div class="contrast-scene-foreground">
  <div class="dark">
    <p>Background #660066 · dark</p>
    <div class="stage"><span class="sample" aria-hidden="true">Aa</span></div>
    <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
  </div>
  <div class="light">
    <p>Background #f5d0fe · light</p>
    <div class="stage"><span class="sample" aria-hidden="true">Aa</span></div>
    <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
  </div>
</div>
:root {
  color-scheme: light dark;
}

.contrast-scene-foreground {

  display: flex;
  flex-wrap: wrap;
  gap: 1rem;

  > div {
    flex: 1 1 12rem;
    min-inline-size: 0;
    display: grid;
    align-content: start;
    gap: 0.5rem;
  }

  p {
    margin: 0;
    color: light-dark(oklch(0.22 0.01 95), oklch(0.95 0.01 95));
    font: 1rem/1.6 system-ui, sans-serif;
    overflow-wrap: anywhere;
  }

  .stage {
    display: grid;
    border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
  }

  .sample {
    display: grid;
    place-items: center;
    min-block-size: 6rem;
    padding: min(1rem, 5vi);
    font: 600 1.5rem/1.6 system-ui, sans-serif;
  }

  @media (forced-colors: active) {
    .stage, .sample {
      color: CanvasText;
      background: Canvas;
      border-color: CanvasText;
    }
  }

  .dark .sample {
    --button-color: #660066;
    --fallback-text-color: white;
    background-color: var(--button-color);
    color: var(--fallback-text-color);
  }

  @supports (color: contrast-color(#660066)) {
    .dark .sample {
      color: contrast-color(var(--button-color));
    }

    .dark .fallback {
      display: none;
    }
  }

  .light .sample {
    --button-color: #f5d0fe;
    --fallback-text-color: black;
    background-color: var(--button-color);
    color: var(--fallback-text-color);
  }

  @supports (color: contrast-color(#f5d0fe)) {
    .light .sample {
      color: contrast-color(var(--button-color));
    }

    .light .fallback {
      display: none;
    }
  }
}

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.

The native selection receives exactly the same argument in both samples; visual contrast changes with the base. Aa is a diagnostic sample, not essential information. A readable label outside each surface explains the case.
The same 20% white over white and soft black
<div class="contrast-scene-backgrounds">
  <div class="light">
    <p>Base #fff · white at 20%</p>
    <div class="stage"><span class="sample" aria-hidden="true">Aa</span></div>
    <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
  </div>
  <div class="dark">
    <p>Base #111 · white at 20%</p>
    <div class="stage"><span class="sample" aria-hidden="true">Aa</span></div>
    <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
  </div>
</div>
:root {
  color-scheme: light dark;
}

.contrast-scene-backgrounds {

  display: flex;
  flex-wrap: wrap;
  gap: 1rem;

  > div {
    flex: 1 1 12rem;
    min-inline-size: 0;
    display: grid;
    align-content: start;
    gap: 0.5rem;
  }

  p {
    margin: 0;
    color: light-dark(oklch(0.22 0.01 95), oklch(0.95 0.01 95));
    font: 1rem/1.6 system-ui, sans-serif;
    overflow-wrap: anywhere;
  }

  .stage {
    display: grid;
    border: 1px solid light-dark(oklch(0.84 0.01 95), oklch(0.36 0.024 260));
  }

  .sample {
    display: grid;
    place-items: center;
    min-block-size: 6rem;
    padding: min(1rem, 5vi);
    font: 600 1.5rem/1.6 system-ui, sans-serif;
  }

  @media (forced-colors: active) {
    .stage, .sample {
      color: CanvasText;
      background: Canvas;
      border-color: CanvasText;
    }
  }

  --wash: rgb(255 255 255 / 0.2);

  .light .stage {
    background-color: #fff;
  }

  .dark .stage {
    background-color: #111;
  }

  .sample {
    background-color: var(--wash);
    color: #000;
  }

  @supports (color: contrast-color(rgb(255 255 255 / 0.2))) {
    .sample {
      color: contrast-color(var(--wash));
    }

    .fallback {
      display: none;
    }
  }
}

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.

The gradient moves from dark to light. Text is always painted over opaque #fef3c7: that is the contrast-color() argument. The text’s base stays valid even if the outer image changes.
Protecting text from a variable background
<div class="contrast-scene-limits">
  <p class="label">An opaque surface defines the real background of this text.</p>
  <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
</div>
.contrast-scene-limits {
  padding: min(1rem, 5vi);
  background: linear-gradient(110deg, #111 50%, #fff 50%);
  font: 1rem/1.6 system-ui, sans-serif;
  overflow-wrap: anywhere;

  .label {
    --surface: #fef3c7;
    margin: 0;
    padding: min(1rem, 5vi);
    background-color: var(--surface);
    color: #000;
  }

  .fallback {
    margin: 0;
    padding: min(1rem, 5vi);
    background-color: #fff;
    color: #000;
  }

  @supports (color: contrast-color(#fef3c7)) {
    .label {
      color: contrast-color(var(--surface));
    }

    .fallback {
      display: none;
    }
  }

  @media (forced-colors: active) {
    background: Canvas;

    .label, .fallback {
      background: Canvas;
      color: CanvasText;
    }
  }
}

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.

Opaque sRGB pair reference · does not predict your browser’s selection
BackgroundWith blackWith white
#6600661.76:1 Below 4.5:111.95:1 Reaches 4.5:1
#f5d0fe15.34:1 Reaches 4.5:11.37:1 Below 4.5:1
#7777774.69:1 Reaches 4.5:14.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.

  1. Get the computed text color and the final background behind it, including transparent layers. A computed background-color with alpha still needs compositing.
  2. 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.
  3. 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.

Hover or focus the button with Tab: its background changes from #660066 to #f5d0fe. Text adapts using the function or the fallback pairs. Clicking the sample does not perform an action.
A button with two color states
<div class="contrast-scene-recipe">
  <button type="button">Try hover and focus</button>
  <p class="fallback">Fallback active: explicit color pair; contrast-color() is not applied.</p>
</div>
.contrast-scene-recipe {
  padding: min(1rem, 5vi);
  background-color: #fff;
  color: #111;
  font: 1rem/1.6 system-ui, sans-serif;
  overflow-wrap: anywhere;

  button {
    --button-bg: #660066;
    --button-fg: #fff;
    min-block-size: 2.75rem;
    max-inline-size: 100%;
    padding: 0.75rem 1.25rem;
    border: 2px solid #660066;
    border-radius: 16px;
    corner-shape: squircle;
    background-color: var(--button-bg);
    color: var(--button-fg);
    font: inherit;
    white-space: normal;
    overflow-wrap: anywhere;

    &:hover, &:focus-visible {
      --button-bg: #f5d0fe;
      --button-fg: #000;
    }

    &:focus-visible {
      outline: 3px solid #111;
      outline-offset: 3px;
    }

    @supports (color: contrast-color(#660066)) {
      color: contrast-color(var(--button-bg));
    }
  }

  .fallback {
    margin-block: 1rem 0;
  }

  @supports (color: contrast-color(#660066)) {
    .fallback {
      display: none;
    }
  }

  @media (forced-colors: active) {
    background: Canvas;
    color: CanvasText;

    button {
      background: ButtonFace;
      color: ButtonText;
      border-color: ButtonText;

      &:focus-visible {
        outline-color: Highlight;
      }
    }
  }
}

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.