DEV Community

Michael Lip
Michael Lip

Posted on • Originally published at zovo.one

WCAG Color Contrast: The Math Behind Accessibility Compliance

A client's marketing team chose a light gray text (#999999) on a white background (#FFFFFF) for their body copy. It looked elegant and minimal. It was also illegible for anyone over 40, anyone in bright ambient lighting, and anyone using a low-quality display. When I measured the contrast ratio, it was 2.85:1. The minimum for body text under WCAG AA is 4.5:1. We failed compliance by a wide margin, and we were one audit away from a lawsuit.

Color contrast is not subjective. It is a measurable ratio defined by a specific algorithm, and there are hard thresholds you either meet or fail.

How contrast ratio is calculated

The WCAG contrast ratio formula compares the relative luminance of two colors:

Contrast Ratio = (L1 + 0.05) / (L2 + 0.05)
Enter fullscreen mode Exit fullscreen mode

Where L1 is the luminance of the lighter color and L2 is the luminance of the darker color. The 0.05 is added to both to prevent division by zero and to account for ambient light reflections on screens.

Relative luminance is calculated from sRGB values using a specific formula. First, convert each 8-bit channel (0-255) to a linear value:

function linearize(channel) {
  const sRGB = channel / 255;
  if (sRGB <= 0.04045) {
    return sRGB / 12.92;
  }
  return Math.pow((sRGB + 0.055) / 1.055, 2.4);
}
Enter fullscreen mode Exit fullscreen mode

Then compute luminance as a weighted sum of the three channels:

function luminance(r, g, b) {
  const R = linearize(r);
  const G = linearize(g);
  const B = linearize(b);
  return 0.2126 * R + 0.7152 * G + 0.0722 * B;
}
Enter fullscreen mode Exit fullscreen mode

The weights (0.2126, 0.7152, 0.0722) reflect the human eye's sensitivity to each primary color. Green contributes the most to perceived brightness, which is why a pure green (#00FF00) looks much brighter than a pure blue (#0000FF) despite both being fully saturated at 255.

Example: black text on white background

White (#FFFFFF): luminance = 1.0
Black (#000000): luminance = 0.0
Contrast = (1.0 + 0.05) / (0.0 + 0.05) = 21:1
Enter fullscreen mode Exit fullscreen mode

The maximum possible contrast ratio is 21:1 (black on white or white on black). The minimum is 1:1 (a color against itself).

Example: that light gray on white

White (#FFFFFF): luminance = 1.0
Gray (#999999): luminance = 0.325
Contrast = (1.0 + 0.05) / (0.325 + 0.05) = 2.80:1
Enter fullscreen mode Exit fullscreen mode

Fails WCAG AA for normal text (requires 4.5:1).

WCAG compliance levels

WCAG defines two conformance levels for contrast:

Level AA (the standard most organizations target):

  • Normal text (under 18pt or under 14pt bold): minimum 4.5:1
  • Large text (18pt+ or 14pt+ bold): minimum 3:1
  • UI components and graphical objects: minimum 3:1

Level AAA (the aspirational level):

  • Normal text: minimum 7:1
  • Large text: minimum 4.5:1

The "large text" exception exists because larger text is inherently more legible due to thicker strokes and greater pixel density. 18pt is approximately 24px at the default browser font size.

The "UI components" requirement (added in WCAG 2.1) means that form borders, icons, focus indicators, and other non-text elements also need sufficient contrast. A text input with a light gray border on white may fail this requirement even if the text inside the input passes.

The colors that commonly fail

After checking hundreds of sites, I see the same failures repeatedly:

Light gray text on white. Any gray lighter than #767676 fails AA against white. If you want gray text on white, #767676 is the lightest gray that passes at 4.5:1. For large text, #949494 passes at 3:1.

White text on yellow or light green. Yellow (#FFFF00) has a luminance of 0.9278 -- almost as bright as white (1.0). The contrast ratio of white on yellow is 1.07:1, virtually invisible. Even dark yellow (#B8860B) only achieves 3.20:1 against white. If you want text on a yellow background, use dark text, not white.

Blue links on black. A common dark-mode failure. Standard blue (#0000FF) on black (#000000) has a contrast ratio of only 2.44:1. You need a lighter blue, like #6495ED (cornflower blue, 4.65:1 against black) to pass.

Placeholder text. HTML input placeholders are often styled with very low contrast to differentiate them from user-entered text. This is a design pattern that directly conflicts with accessibility requirements. If placeholder text conveys important information (like the expected date format), it needs to pass contrast requirements.

Text over images. A banner with white text over a photograph fails wherever the photo is bright. Solutions: add a semi-transparent overlay, use a text shadow, or place text in an opaque container. You cannot check contrast against a photograph with a single number -- it varies per pixel.

Practical tips for passing contrast

Start with your text colors. Choose your smallest body text color first. On a white background, any color with luminance below approximately 0.18 will pass AA. On a black background, any color with luminance above approximately 0.30 will pass. Build your palette around these anchors.

Do not darken text to fix contrast -- lighten the background or use a different hue. If your brand color is a medium blue that fails against white, do not switch to a darker blue that looks muddy. Instead, try the brand blue on a very light blue background, where a lower contrast ratio may still pass because the background luminance is lower than pure white.

Check all states. A button might have good contrast in its default state but fail on hover, active, disabled, or focus states. Disabled elements are exempt from WCAG contrast requirements, but if users need to read the label to understand what the disabled button does, low contrast is still a usability problem.

Test in context, not in isolation. A color pair might pass the mathematical threshold but still be hard to read at 12px font size on a mobile screen in sunlight. Contrast ratios are a minimum floor, not a guarantee of legibility. When in doubt, go higher.

Use your browser's built-in tools. Chrome DevTools shows the contrast ratio in the color picker when you inspect an element. It also flags elements that fail WCAG in the Accessibility audit. Firefox's Accessibility Inspector highlights contrast failures directly in the page.

When I am evaluating color pairs during design or reviewing pull requests that change colors, I use the color contrast checker at zovo.one/free-tools/color-contrast-checker to verify compliance before merging.

Accessibility lawsuits are increasing. The ADA has been interpreted to apply to websites, and contrast failures are among the easiest issues for auditors to flag because they are objectively measurable. But the real reason to care about contrast is simpler: text that people cannot read is text that does not exist. And you wrote it for a reason.


I'm Michael Lip. I build free developer tools at zovo.one. 350+ tools, all private, all free.

Top comments (0)