WCAG color contrast made simple
How contrast ratios work, what the AA and AAA thresholds mean, common pitfalls with placeholder text and disabled states, and practical tips for accessible palettes.
What contrast ratio actually measures
The WCAG contrast ratio is a number between 1:1 (no contrast — same color on itself) and 21:1 (maximum contrast — pure black on pure white). It’s computed from the relative luminance of two colors — a measure of how bright a color appears to the human visual system, normalized from 0 (black) to 1 (white).
The formula is straightforward:
contrast = (L1 + 0.05) / (L2 + 0.05)
where L1 is the luminance of the lighter color and L2 is the luminance of the darker one. The 0.05 terms prevent division by zero and account for ambient light reflected off screens.
Computing relative luminance
Relative luminance is not a simple average of RGB channels. It’s a weighted sum that models how the human eye perceives brightness — we’re far more sensitive to green light than red, and far more sensitive to red than blue.
The computation has two steps. First, linearize each sRGB channel by removing the gamma encoding:
function linearize(c: number): number {
// c is 0-1 (e.g., 128/255 = 0.502)
return c <= 0.04045
? c / 12.92
: Math.pow((c + 0.055) / 1.055, 2.4);
}
Then compute the weighted sum:
L = 0.2126 * linearize(R) + 0.7152 * linearize(G) + 0.0722 * linearize(B)
The weights come from the ITU-R BT.709 standard (the same one used for HDTV). Green gets 71.52% of the weight, red gets 21.26%, blue gets 7.22%. This is why pure green (#00ff00) appears far brighter than pure blue (#0000ff) despite both being at maximum channel intensity.
A common mistake is using the simpler perceived-brightness formula (0.299R + 0.587G + 0.114B). That formula (from ITU-R BT.601) is fine for quick foreground-text color decisions, but it skips the gamma linearization step and uses different weights. WCAG explicitly requires the BT.709 formula with linearization. Using the wrong formula will give you slightly different contrast ratios and may cause you to pass colors that actually fail.
The AA and AAA thresholds
WCAG 2.1 defines two conformance levels for text contrast, each with a “normal text” and “large text” threshold:
- AA normal text: ≥ 4.5:1
- AA large text: ≥ 3:1
- AAA normal text: ≥ 7:1
- AAA large text: ≥ 4.5:1
Large text is defined as 18pt (24px) or larger, or 14pt (roughly 18.5px) bold or larger. Everything else is normal text.
AA is the baseline that most accessibility laws reference. Meeting AA means your text is legible for the majority of users, including those with moderate low vision. AAA is the gold standard — it provides enough contrast for users with more significant vision impairments. Most design systems aim for AA across the board and hit AAA where possible (body text is easy; subtle UI elements are harder).
Non-text contrast (WCAG 2.1, 1.4.11)
WCAG 2.1 added a requirement for non-text elements — UI components (buttons, form fields, icons) and graphical objects need a minimum 3:1 contrast ratio against adjacent colors. This is a separate criterion from text contrast, but it uses the same ratio math.
This means your button border needs 3:1 against the background, your icon fill needs 3:1 against its surroundings, and your focus ring needs 3:1 against the background it sits on. It’s a lower bar than text AA, but it catches a surprising number of real-world failures.
Common pitfalls
Placeholder text
Placeholder text in form fields is notorious for failing contrast. The default browser placeholder color is typically a light gray (#a0a0a0 or similar) on a white background — which comes out to about 2.6:1. That fails AA for normal text.
The fix isn’t to make placeholders darker (that makes them look like entered values), but to use visible labels instead. Placeholders should be treated as supplementary hints, not the primary label. If you do style placeholders, #767676 on white gives you exactly 4.5:1.
Disabled states
WCAG explicitly exempts “inactive user interface components” from contrast requirements. Disabled buttons can be as faint as you want. But this exemption is narrower than it seems — the user still needs to perceive that a control exists and is disabled. A button that’s visually indistinguishable from the background is worse than a low-contrast one. Use 2:1 or better for disabled elements as a practical minimum, even though it isn’t strictly required.
Color as the only signal
WCAG 1.4.1 says color must not be the only visual means of conveying information. A form validation error shown only as a red border fails — a user who can’t distinguish red from gray won’t know the field is in error. The fix: add an icon, text message, or pattern alongside the color change.
This applies to contrast checkers themselves. A pass/fail indicator that’s only green or red is inaccessible. Always pair the color with a text label (“Pass” / “Fail”) and an icon (checkmark / X). The Contrast Checker tool follows this pattern — every result shows an icon plus text, not just a colored badge.
Text over images and gradients
When text sits on a photo or gradient, contrast varies by pixel. WCAG requires the contrast requirement to be met at the worst point — the lightest pixel behind dark text, or the darkest pixel behind light text. In practice, this means you need a scrim (semi-transparent overlay) between the image and the text. A background: linear-gradient(transparent, rgba(0,0,0,0.7)) overlay on a hero image ensures white text at the bottom has enough contrast regardless of the photo content.
Practical tips for maintaining accessible palettes
Start with your text colors
Design your lightest background and your body text color first. Ensure they hit 7:1 (AAA) — this gives you headroom. Then derive your secondary text color at 4.5:1 minimum. If your background is #ffffff, your body text could be #1a1a1a (19.3:1) and your secondary text #595959 (7.0:1).
Check early and often
Don’t defer contrast checking to a final accessibility audit. Bake it into your design process: every time you pick a new color pair, run it through a contrast check. Catching a failure early — when a slight hue or lightness tweak is easy — is far less painful than redesigning an entire component after launch.
Use HSL lightness as a quick gut check
A rough rule of thumb: text and background need at least a 40-50 point difference in HSL lightness to pass AA for normal text. This isn’t exact (saturation and hue affect relative luminance), but it’s a fast sanity check before running the real formula.
Build a contrast matrix
For design systems with multiple surface colors and text colors, build a matrix: every background in one axis, every text color in the other, each cell showing the contrast ratio and pass/fail status. This matrix becomes the single source of truth for which combinations are permitted. It’s tedious to build once, but it prevents ad-hoc color choices that slowly erode accessibility.
Check your colors
The Contrast Checker computes the exact WCAG 2.1 contrast ratio for any foreground/background pair and shows AA/AAA pass/fail results for both normal and large text. Use it every time you’re evaluating a color combination.
Ready to put this into practice?
Open the tool →