WCAG Color Contrast Guide

What the ratios mean, where AA ends and AAA begins, why mid-gray on white just barely fails — and how to fix a palette that doesn't pass.

The 4.5:1 threshold for normal text isn't arbitrary. It approximates the contrast level at which text stays readable for people with low vision, on low-quality screens, in bright ambient light — conditions that describe far more of your audience than most teams assume. WCAG color contrast is also the easiest accessibility criterion to check and fix, which makes it the best place to start. This guide covers the math, the thresholds, the common failures and the fixes.

How contrast ratio is calculated

WCAG first converts each color to relative luminance: the red, green and blue channels are linearized (gamma removed), then weighted and summed — roughly 21% red, 72% green, 7% blue — producing a value between 0 (black) and 1 (white). Green dominates the sum, which is why yellow-green hues always look "brighter" than blues of the same saturation.

The contrast ratio is then (L1 + 0.05) / (L2 + 0.05), where L1 is the lighter of the two luminances. The result spans 1:1 (identical colors) to 21:1 (pure black on pure white). Two anchors are worth memorizing: black on white is exactly 21:1, and mid-gray #777777 on white works out to roughly 4.48:1 — which fails the 4.5:1 AA threshold by a hair. That's the trap of eyeballing: #777 looks acceptably readable, but the threshold is exact, and "looks fine" is not a measurement. The generator computes these ratios on-device as you adjust colors — how it works has the details.

The thresholds

Text typeAAAAA
Normal text (<24px)4.5:17:1
Large text (≥24px, or ≥18.66px bold)3:14.5:1
UI components & graphical objects3:1—

AA is the legal baseline in most jurisdictions; AAA is the enhanced tier. A pragmatic target: AAA for body text on reading-heavy pages, AA everywhere else, and 3:1 as the floor for anything visual that carries meaning.

What counts as large text

Large text means at least 24px regular weight, or at least 18.66px bold (14pt) — the browser's computed CSS size, not the font file's nominal size. In default root sizing, 24px is 1.5rem, but don't rely on the conversion alone: responsive rules, zoom and inherited styles change the computed size. The exception is narrower than teams hope: it never covers form labels, captions, button labels at common sizes or anything that reflows smaller on mobile. Treat 4.5:1 as the default for every pair where you can't guarantee both the size and the weight at all breakpoints.

UI components and non-text contrast

WCAG 1.4.11 extends the idea beyond text: any visual information needed to identify a UI component or its state must reach 3:1 against adjacent colors. In practice that includes:

  • Button and input boundaries, when the outline is the only thing marking them as interactive.
  • Icons that carry meaning — a hamburger menu, a close cross, a download glyph.
  • Focus indicators, checkbox and radio borders, slider handles.
  • State changes conveyed only by color, such as a filled versus empty input border.

If a component would disappear into the background when its text is removed, its boundary needs to pass 3:1.

Common failures

Gray text on white

The classic. #999999 on white is about 2.85:1, and even #777777 lands at 4.48:1 — a fail for normal text. Muted text is still text: captions, timestamps and helper copy all need 4.5:1.

Low-contrast placeholders

Placeholder text is subject to the same thresholds, and it's usually styled lighter than any other text on the page. Style it to pass, and remember it vanishes on input — labels shouldn't live inside the field.

Colored buttons with white text

Bright brand colors often fail with white labels. White on amber #F59E0B, for example, is roughly 2.15:1 — a severe fail. The fix is dark text on that button, or a darker variant of the brand color.

Light borders

A hairline #EEEEEE border on a white card is fine when purely decorative. When the border marks an input or a clickable card, it's functional and needs 3:1.

Disabled ≠ exempt from good practice

WCAG exempts disabled controls, but a disabled button nobody can perceive isn't accessible in any useful sense. Keep disabled states visible — reduced saturation and opacity, but still legible — and mark the state with more than color.

How to fix a failing palette

Work through the options in order — they're ranked by how little they disturb the design:

  1. Adjust lightness, not hue. Luminance lives mostly in lightness, so darken the text or lighten the background in HSL terms until the ratio passes. Hue swaps rarely fix contrast and usually break the palette's harmony.
  2. Swap foreground and background. Dark text on a light amber button passes where white text fails, with no other change.
  3. Add an overlay. For text over images, a semi-transparent scrim between image and text moves the effective background toward one end of the scale.
  4. Use the large-text exception — only if it's real. If the text genuinely holds 24px (or 18.66px bold) at every breakpoint, 3:1 suffices.

Re-measure after every change. The contrast checker shows the live ratio and AA/AAA pass badges as you adjust, so you can nudge a color and watch the verdict flip in real time. And if the palette itself is still in flux, it's faster to rebuild it with contrast-aware colors in the free AI color palette generator than to patch a failing one.

Beyond the checklist

Passing ratios is table stakes, not the finish line. WCAG 1.4.1 (use of color) forbids color as the only carrier of meaning: pair error red with an icon and a message, link text with underlines, chart series with labels or patterns. And test with real content — real words, real weights, real component states — because a palette that passes with lorem ipsum in a mockup can fail the moment genuine content lands. Re-run the check whenever the palette, the theme or the typography changes; contrast is a property of pairs, not of colors in isolation. For the system-level view of where each color belongs, see the guide to semantic UI color roles.

Check your palette now: run every pair through the free WCAG contrast checker in ChromaKit — live ratios, AA and AAA badges, no account needed.