WCAG badge
wcag-badge overlays a live WCAG contrast rating on any element with a background and text color. The badge shows AAA, AA, AA Large or Fail, all computed in CSS at render time. Paired with text-contrast-* it adds a fifth state, Max, for a level the background cannot physically reach.
Browser Support
Needs CSS @function and if(style()), which today means Chromium only. The utility sits inside @supports, so anywhere else it renders nothing.
This does not apply to its partner text-contrast-*, which is stable and works everywhere. The badge is the half of the module that cannot follow: reading a rating out means turning a color into a string, and if(style()) is the only way CSS can do that. So an element carrying both utilities gets automatic contrast in every browser and a badge only in Chromium.
Quick reference
| class | styles |
|---|---|
| wcag-badge | ::after badge showing AAA / AA / AA Large / Fail (or Max, with text-contrast-*) |
Basic usage
Add wcag-badge to any element that has both a background color (bg-*) and text color (text-*):
The badge background color is green (AAA), yellow (AA), orange (AA Large), or red (Fail, and Max). Badge text is black on yellow (AA), white on all others.
Across the spectrum
White text on various backgrounds:
Dark text on light backgrounds:
Combining with text-contrast-*
wcag-badge and text-contrast-* work together on the same element. The badge reads the text color text-contrast-* set and reports the rating that comes out:
Both utilities update live when the background changes. Hover to watch the text and badge recompute:
Max, the level that was impossible
Some ratios cannot be reached from some backgrounds. The most any background can offer is max((Y + 0.05)/0.05, 1.05/(Y + 0.05)), which bottoms out at √21 ≈ 4.583 at the 0.1791 pivot, so 3:1 and 4.5:1 are always achievable, but 7:1 is out of reach for backgrounds in the middle of the luminance range. There the shade saturates at pure black or white and delivers the best that exists.
Without a fifth state, that case reads as a plain AA, indistinguishable from "your AAA request quietly didn't work". Max says the quiet part out loud: this is the ceiling, and asking for more will not help.
Two things follow from how it is derived:
- It needs
text-contrast-*. Max describes a shortfall against a requested level, and onlytext-contrast-*records what was requested. A barewcag-badgehas no intent to compare against, so it never shows Max. It reports plain measurement, exactly as before. - It shares Fail's red. Both mean "don't ship this as-is". The difference is what to do about it: Fail means fix the pair, Max means pick a different background or accept a lower level.
Badge must be on the text color element
wcag-badge reads --jib-text-color from the element it's placed on. It measures the contrast between its own background and text color, and cannot see or predict the contrast of child elements.
Place the badge on the same element that has the text color, or on a child element that carries the text color. Do not place it on a parent and expect it to measure a child's text-contrast-* color.
How it works
The badge reads the captured --jib-background-color and --jib-text-color custom properties (set by bg-* and text-* utilities in core/_index.css), and runs them through an exact contrast pipeline:
--jib-luminance-packed(). Packs each color's relative luminance into R and its complement into G, so a singlecolor-mix()becomes a luminance subtraction.--jib-contrast-test-all(). A multi-channelcolor-mix()tests all three WCAG thresholds (3, 4.5, 7) at once. Each output channel encodes one threshold; running both directional orderings handles either-can-be-lighter without branching.--jib-wcag-rating(). Matches the result color against the four exact states: white = AAA, yellow = AA, red = AA Large, black = Fail (returns<string>).--jib-wcag-shortfall. Compares the rating against the leveltext-contrast-*recorded in--jib-contrast-level, which is what yields the Max state described above.::afterpseudo-element.content: var(--jib-wcag-display)displays the rating, orMax.- Conditional badge color.
if(style())maps the displayed value to green/yellow/orange/red using TW color tokens; badge text is black on yellow (AA), white on all others.
Because the comparison stays in color space the whole way through, never extracting a numeric luminance, there is no quantization error and no uncertainty band. Every step works in srgb-linear; routing the luminance through the legacy rgb() function instead costs 10⁻⁵-scale precision, always signed so the darker color of a pair measures darker, which is enough to decide a verdict for a pair sitting on a threshold.
There is one deliberate tolerance. WCAG asks for a ratio of at least the threshold, so a pair landing exactly on 4.5:1 is AA, but a step function returns "fail" at exactly zero. The thresholds therefore carry a 10⁻⁶ epsilon, about 5 × 10⁻⁴ in ratio terms, so an exact tie resolves in favor of passing. This matters because text-contrast-* produces exact ties by design; without it the badge would contradict the class beside it on every element.
Import
Included in @import 'tw-jib-css-experimental'. To take it on its own, with no other experimental module, and none of the @function overrides:
@import 'tw-jib-css-experimental/wcag-badge';Taking the badge deliberately does not reroute text-contrast-* onto @function. The badge reads --jib-contrast-level, which both shade paths write, so it reports correctly either way. Automatic contrast itself is separate and stable: @import 'tw-jib-css'.
Baseline status
What these utilities are built on, live from the Web Platform Dashboard.