HandyTools Hub

← All guides

Color Contrast on the Web: A Practical Guide to WCAG

2026-08-06

If you’ve ever run an accessibility audit, you’ve probably hit this moment: a paragraph of light-gray text that looks perfectly readable to you gets flagged for “insufficient contrast.” The tool isn’t being pedantic — human judgment about readability is notoriously unreliable. It’s affected by screen brightness, ambient light, age, and how tired your eyes are that day. WCAG (the Web Content Accessibility Guidelines) replaces subjective judgment with a formula. This article explains that formula and how to work with it in practice.

Relative luminance: where the math starts

WCAG measures contrast using relative luminance — a number between 0 (black) and 1 (white) that approximates how bright a color appears to the human eye. It is not a simple average of the RGB channels. Each channel value (0–255) is first normalized to 0–1, then “linearized” (divided by 12.92 if it’s at or below 0.04045, otherwise run through a power function with exponent 2.4), and finally combined with weights:

L = 0.2126 × R + 0.7152 × G + 0.0722 × B

The weights are wildly uneven: green accounts for 71.5%, red for 21.3%, and blue for just 7.2%. That mirrors the sensitivity of the human eye, which is most responsive to green and least to blue. It’s also why pure blue and pure green of the same “intensity” behave so differently — black text on a dark blue background can be tolerable, while black text on saturated green is painful.

How the contrast ratio is computed

Once you have the relative luminance of the foreground (L1, the lighter one) and the background (L2), the ratio is:

ratio = (L1 + 0.05) / (L2 + 0.05)

The result ranges from 1:1 (identical colors) to 21:1 (pure black on pure white). The 0.05 term compensates for ambient light and keeps the ratio from exploding with very dark colors. The ratio depends only on the two colors — font size and weight don’t enter the calculation. They matter separately, in the thresholds.

AA vs. AAA: two levels of compliance

WCAG 2.1 defines three conformance levels — A, AA, and AAA. Contrast requirements live in the latter two:

LevelNormal body textLarge text
AA (the mainstream requirement)4.5:13:1
AAA (enhanced)7:14.5:1

Most laws and procurement standards (EN 301 549 in the EU, ADA-related practice in the US) require AA. AAA is a stretch goal, typically mandated only where readability is critical, such as healthcare or government services. A pragmatic strategy: guarantee AA for body text, and aim for AAA on headlines and key calls to action.

A few element types are exempt from contrast requirements: purely decorative content, invisible elements, disabled controls, and text inside logos and brand marks. But “exempt” doesn’t mean “a good idea” — if disabled text can be readable without visual confusion, make it readable.

Why body text and large text have different thresholds

“Large text” is defined as at least 18pt (about 24px) at regular weight, or 14pt (about 18.66px) bold. Bigger glyphs with thicker strokes need less contrast to stay legible, so AA only asks for 3:1 on large text.

The practical consequence cuts both ways: oversized headlines can safely use lighter, more expressive colors, while body copy must stay conservative. And the classic failure zone is the 13px light-gray caption — it must meet the full 4.5:1, yet it’s exactly where designers most often dial the color down on instinct.

Why gray text keeps failing

What’s #777777 on white? 4.48:1 — a hair under 4.5. This famous near-miss illustrates the core problem: grays picked by eyeball tend to land in the danger zone right around the threshold. Other frequent offenders: #999999 (2.85:1, large text only) and #CCCCCC (1.6:1, essentially unreadable).

Gray text is popular because it makes interfaces feel “light,” but the perceptual distance between a light gray and white is short, leaving very little room to maneuver. If your design needs de-emphasized secondary text, the right workflow is to fix the background first, then start from a dark gray and lighten it step by step until it just clears the line — not to type in a gray that feels about right. When in doubt, run the pair through a color contrast checker; it evaluates AA and AAA for both normal and large text in one shot, which beats mental arithmetic every time.

What to do when your brand color fails

Brand colors are the most common and most awkward source of contrast conflicts: marketing’s primary color sits at 3.8:1, changing it requires brand-team approval, and shipping it as-is fails audits. Here are battle-tested options, ordered from cheapest to most expensive:

  1. Reserve the brand color for large text and graphics. Anything at or above 3:1 can legally appear in headlines, icons, and button borders — just not in body copy.
  2. Derive a “text variant” of the brand color. Keep the hue and saturation, darken only the lightness, and you get a variant that passes 4.5:1 while still reading as the same brand color to users. A color picker makes this easy — hold the hue steady, nudge the lightness, and watch the contrast as you go.
  3. Change the background instead of the foreground. Dark brand colors score slightly better on an off-white like #F5F5F5 than on pure white, and sometimes that margin is enough. The direction is counterintuitive: darkening the background raises the ratio for dark text.
  4. Put dark text on the brand color. If the brand color is bright, black text on it usually beats white text by a wide margin.

The worst option is shipping the brand color on 14px body text and pretending the audit report doesn’t exist.

A contrast-checking workflow for design systems

Ad-hoc checks can’t fix systemic problems. If your team maintains a design system, turn contrast checking into a process rather than a personal habit:

  • Test at the token table, not the page. Design-system colors usually live as tokens (text-primary, surface-muted). Enumerate every legal “text color × background color” combination, measure each one, and ban the failing pairs from the spec outright.
  • Annotate every text token with its intended use. Mark whether a color is “body-safe” (must pass 4.5:1) or “large/decorative only” (3:1), so nobody promotes a caption gray into paragraphs.
  • Bake it into the component library. Buttons, inputs, and cards get checked once at the component level; product teams then inherit compliant defaults for free.
  • Re-run the matrix on redesigns. Brand refreshes and dark-mode launches are classic regression points. Dark mode fails especially often — pure white text on a light-gray card is a subtler problem than anything in light mode.

If you’re still at the palette stage, generate candidates with a color palette generator and filter them through contrast checks before committing — it’s far more efficient than patching a failing brand palette after the fact.

Wrapping up

Contrast isn’t mysticism: one luminance formula plus two tiers of thresholds (AA 4.5/3, AAA 7/4.5) is the entire rulebook. Gray text is the usual suspect, brand colors get compliant through derived variants or restricted usage, and pushing checks upstream into the design system saves an order of magnitude of rework. The next time a design review devolves into “I can read it fine,” paste the two hex values into a checker and let the numbers settle it.