WCAG color contrast is measured as a contrast ratio between 1:1 and 21:1, calculated from the relative luminance of the text color and the background color. To meet WCAG 2.2 Level AA, normal text needs a ratio of at least 4.5:1 and large text at least 3:1; Level AAA raises those to 7:1 and 4.5:1.
Low-contrast text is one of the most common accessibility failures on the web, and one of the easiest to fix once you know the numbers. This guide covers the exact formula from WCAG 2.2, what counts as large text, the separate 3:1 rule for icons and form borders, a set of computed example pairs, and the mistakes that make a color pair fail an audit even when it looks fine on your screen. To test your own colors, use the Contrast Checker.
What a contrast ratio is
The W3C defines the contrast ratio as:
contrast ratio = (L1 + 0.05) / (L2 + 0.05)
Here L1 is the relative luminance of the lighter color and L2 is the relative luminance of the darker one. Luminance runs from 0 for black to 1 for white, so the result ranges from 1:1 (two identical colors) to (1 + 0.05) / (0 + 0.05) = 21:1 (black on white). Because the lighter color always goes on top, the ratio doesn't care which color is text and which is background: white text on blue scores exactly the same as blue text on white.
The ratio comes from measured luminance, not from how different two colors look. Two colors with very different hues, like red and green, can have similar luminance and a low ratio.
WCAG contrast requirements: AA and AAA
| What | Level AA | Level AAA | Success criterion |
|---|---|---|---|
| Normal text | 4.5:1 | 7:1 | 1.4.3 (AA), 1.4.6 (AAA) |
| Large text | 3:1 | 4.5:1 | 1.4.3 (AA), 1.4.6 (AAA) |
| UI components and graphics | 3:1 | No separate AAA level | 1.4.11 Non-text Contrast (AA) |
The W3C's Understanding 1.4.3 explains that 4.5:1 was chosen because it compensates for the loss in contrast sensitivity typical of people with vision of roughly 20/40. It also makes two rules explicit:
- Don't round. Computed ratios "should not be rounded", so 4.499:1 does not meet 4.5:1.
- Some text is exempt: text in logos and brand names, purely decorative text, text that isn't visible to anyone, text in inactive (disabled) components, and text that's part of a picture with significant other visual content.
AA is the level legal and procurement rules reference in practice. In the EU, EN 301 549 incorporates WCAG 2.1 Level AA for web content. In the U.S., the ADA itself doesn't name a technical standard, but the Department of Justice's 2024 rule for state and local government websites adopts WCAG 2.1 Level AA. AAA is a stricter bar that's hard to hit with many brand palettes and isn't usually required.
What counts as large text
WCAG defines large-scale text as at least 18 point, or at least 14 point bold. Understanding 1.4.3 gives the CSS conversion as 1pt = 1.333px, so:
- 18pt regular ≈ 24px
- 14pt bold ≈ 18.5px
Anything smaller is normal text and needs the full 4.5:1 at AA. That means a typical 16px body font, or an 18px regular heading, is not large text, even though it may look generous on screen. The sizes refer to the rendered text, so the W3C advises getting the size from the browser rather than from a design file.
How to calculate relative luminance
The WCAG 2.2 definition of relative luminance works on sRGB colors in three steps:
- Scale each channel to 0–1: divide the 8-bit value by 255.
- Linearize it: if the value is at or below 0.04045, divide by 12.92; otherwise compute
((c + 0.055) / 1.055) ^ 2.4. - Weight the channels:
L = 0.2126 × R + 0.7152 × G + 0.0722 × B.
Older versions of WCAG used 0.03928 as the cutoff in step 2. The W3C updated it in May 2021 and notes that the change has no practical effect. We checked: no 8-bit channel value (0 to 255) falls between the two cutoffs, so both give identical results for every hex color. The whole calculation fits in a few lines of Python:
def lin(v):
c = v / 255
return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
def luminance(hex_color):
r, g, b = (int(hex_color.lstrip("#")[i:i+2], 16) for i in (0, 2, 4))
return 0.2126 * lin(r) + 0.7152 * lin(g) + 0.0722 * lin(b)
def contrast(a, b):
hi, lo = sorted([luminance(a), luminance(b)], reverse=True)
return (hi + 0.05) / (lo + 0.05)
contrast("#767676", "#FFFFFF") # 4.542...
A worked example: #767676 on white
- Hex 76 is 118 in decimal, and 118 ÷ 255 ≈ 0.4627. It's a gray, so all three channels are the same.
- 0.4627 is above 0.04045, so linearize: ((0.4627 + 0.055) / 1.055)2.4 ≈ 0.1812.
- With all three channels at 0.1812 the weights sum to 1, so L ≈ 0.1812. White is 1.
- (1 + 0.05) / (0.1812 + 0.05) ≈ 4.54:1, a pass for normal text at AA.
One step lighter, #777777, gives 4.478:1 and fails. A single hex digit separates pass from fail, and no one can see that difference by eye.
Contrast ratio examples
Every ratio below was computed with the Python above and rounded to two decimals for display. Pass or fail is judged on the unrounded value.
| Foreground | Background | Ratio | Result |
|---|---|---|---|
#000000 | #FFFFFF | 21:1 | Maximum possible |
#333333 | #FFFFFF | 12.63:1 | Passes AA and AAA |
#595959 | #FFFFFF | 7.00:1 | Just passes AAA normal text |
#767676 | #FFFFFF | 4.54:1 | Passes AA normal text |
#777777 | #FFFFFF | 4.48:1 | Fails AA normal text; passes large |
#999999 | #FFFFFF | 2.85:1 | Fails even large text |
#FFFFFF | #2563EB | 5.17:1 | Passes AA at any size |
#FFFFFF | #3B82F6 | 3.68:1 | Large text only |
#FFFFFF | #DC2626 | 4.83:1 | Passes AA |
#000000 | #DC2626 | 4.35:1 | Large text only |
#0000FF | #FFFFFF | 8.59:1 | Passes AAA |
#00FF00 | #FFFFFF | 1.37:1 | Fails everything |
A few lessons from the table. Green carries most of a color's luminance, which is why pure green on white is nearly invisible while pure blue passes AAA. The step from #3B82F6 to #2563EB is small in HSL lightness (60% to 53%) but moves a button label from "large text only" to a pass at any size. And a 50% black overlay on the white parts of a photo produces #808080, on which white text manages only about 3.95:1, so test text on images against the lightest area it actually crosses.
Non-text contrast (1.4.11)
Text isn't the only thing that needs contrast. Success criterion 1.4.11 Non-text Contrast, a Level AA requirement added in WCAG 2.1, requires a ratio of at least 3:1 against adjacent colors for:
- User interface components: the visual information needed to identify a control and its state, such as an input's border, a checkbox outline, a toggle's on/off state or a focus indicator.
- Graphical objects: the parts of a graphic needed to understand it, such as icons without text, lines in a chart or the segments of a pie chart.
Inactive components are exempt, as are controls whose appearance is set by the browser and not modified by the author, and graphics where a particular presentation is essential to the information. In practice, a #949494 input border on white measures 3.03:1 and passes, while a popular pale #CCCCCC border measures only 1.61:1, leaving the field hard to find for some users.
Common fixes when a pair fails
- Adjust lightness, not hue. Darken light text on a light background, or lighten dark text on a dark one. A few percentage points of HSL lightness is often enough. The Color Converter shows a color's HSL form, which is the easiest place to nudge lightness while keeping the hue.
- Make the text genuinely large if the design allows: 24px regular or about 18.5px bold qualifies for 3:1.
- Add a solid or semi-opaque background behind text placed over images or gradients.
- Don't rely on color alone to convey meaning, like red and green status. Pair color with an icon or label. That's a separate requirement, 1.4.1 Use of Color, but closely related.
Mistakes that slip through reviews
- Checking the design-file color instead of the rendered one. Opacity, overlays and blend modes change the final color. If text is set at 70% opacity, the color that matters is the blended result.
- Only testing the default state. Hover, focus, visited links, error messages and dark mode each create new color pairs.
- Rounding before comparing.
#777777on white is 4.478:1, which rounds to 4.5 at one decimal place, but it still fails AA for normal text. - Forgetting non-text elements. Every label can pass while the input borders fail 1.4.11.
A quick contrast checklist
- Body text: at least 4.5:1 (7:1 for AAA).
- Large text, 24px regular or about 18.5px bold and up: at least 3:1 (4.5:1 for AAA).
- Borders, icons, focus rings and chart elements users need to perceive: at least 3:1 against adjacent colors.
- Every interactive state and theme checked, not just the default.
- Text over images checked against the lightest (or darkest) area it overlaps.
- Meaning never carried by color alone.
Enter a text and background color to get the exact ratio and AA/AAA pass or fail.
Open Contrast Checker →If you're pulling colors from a screenshot or mockup, the Image Color Picker gives you the exact HEX value to test.
Frequently asked questions
What contrast ratio does WCAG AA require?
WCAG 2.2 Level AA requires a contrast ratio of at least 4.5:1 for normal text and 3:1 for large text, which is at least 18 point (about 24px) or 14 point bold (about 18.5px). User interface components and meaningful graphics need at least 3:1 against adjacent colors.
How is the contrast ratio calculated?
Compute the relative luminance of both colors, then divide the lighter one plus 0.05 by the darker one plus 0.05. Relative luminance weights linearized red, green and blue as 0.2126, 0.7152 and 0.0722. The result runs from 1:1 to 21:1.
What counts as large text in WCAG?
Text that is at least 18 point, or at least 14 point and bold. Using the W3C conversion of 1pt = 1.333px, that is about 24px for regular text and about 18.5px for bold text. A 16px or 18px regular font is not large text.
Does WCAG 2.2 change the color contrast requirements?
No. The contrast criteria — 1.4.3 Contrast (Minimum), 1.4.6 Contrast (Enhanced), and 1.4.11 Non-text Contrast — carry over from WCAG 2.1 with the same 4.5:1, 7:1, and 3:1 thresholds. The criteria that WCAG 2.2 added focus on other areas, such as focus visibility, target size, and accessible authentication.
Is 4.49:1 close enough to pass WCAG AA?
No. WCAG treats 4.5:1 as a hard threshold, and the W3C's guidance says computed ratios should not be rounded, so even 4.499:1 fails. If a pair lands just under the line, darken or lighten one of the colors slightly until the unrounded ratio reaches 4.5:1.
Do logos and disabled buttons need to meet contrast requirements?
No. Text that is part of a logo or brand name has no contrast requirement, and neither do inactive user interface components such as a disabled button. Purely decorative text is also exempt, but any text a user needs to read, including captions and helper text, is covered.