2026-10-14 · 8 min read
- accessibility
- css
- color
WCAG 2 contrast is a ratio of relative luminances. You linearize the red, green, and blue channels of an opaque sRGB color, weight them, add 0.05 to each luminance, and divide the lighter by the darker. The result is a number you compare with a threshold. It is not the distance between two token names.
The threshold depends on the role. Under WCAG 2.1 AA, ordinary text needs 4.5:1, and large text plus meaningful non-text boundaries need 3:1. AAA asks for 7:1 and 4.5:1 for those text cases. Pick the threshold before you sample the color.
Sample what the user sees
Opacity, blend modes, overlays, and background images change the pixels. Composite the foreground over the background that is actually behind the words, then convert that result to an opaque sRGB hex. Do this for the resting state, the hover state, and the theme you ship.
A gradient or a photograph does not have one ratio. Measure the region where the text is hardest to see and treat that region as the result. A pass on the left edge does not cover the right edge.
- Flatten alpha before measuring.
- Use the painted hex, not the source token.
- Record text size so the threshold is explicit.
- Repeat the sample in the dark theme.
Use one linearization cutoff on purpose
The channel is linearized with a cutoff. WCAG 2.0 and 2.1 text used 0.03928. WCAG 2.2 uses 0.04045 so the step matches sRGB. Colors far from that band produce the same ratio either way. Colors near a 4.5:1 or 3:1 boundary can change pass or fail when the cutoff changes.
Flashman's color tool uses 0.03928 and two opaque hex colors. If a design-system checker uses 0.04045, run both on the boundary pair and write down which cutoff the release uses. Do not average the two ratios.
Do not substitute a perceptual difference
OKLCH lightness and APCA are useful for designing ramps. They are not the WCAG 2 ratio. A pair that looks evenly spaced in a perceptual space can still fail 4.5:1, and a pair that passes 4.5:1 can still be a poor choice for small text on a busy background. Meet the ratio you claimed, and look at the text.
Disabled text is often exempt as inactive, and that exemption is easy to overuse. If the disabled label is the only instruction on the control, treat it as text someone must read and measure it.
Keep the pair next to the component
Store foreground, background, opacity, theme, cutoff, and ratio beside the component. A later token rename should not erase the evidence. When the ratio moves, diff the old pair and the new pair rather than the token diff alone.
Use the color tool on the opaque hex pair, the CSS formatter to find opacity and overlays, the JSON formatter for the token file, and diff for the two checkers. The number in the issue should be reproducible from those inputs.