Originally published at AccessGuard.
Color contrast is the most common accessibility failure
If there's one accessibility issue that appears on nearly every website, it's insufficient color contrast. The WebAIM Million study found low contrast text on 83.9% of the home pages it tested in 2026, up from 79.1% in 2025, with an average of 34 distinct instances per home page. That makes it the single most widespread WCAG failure on the web.
The reason is straightforward: designers choose colors that look good on their own high-resolution monitors in well-lit offices. For anyone with low vision, a color vision deficiency or age-related sight changes, those subtle grays and trendy pastels can render text unreadable.
This guide covers every WCAG contrast threshold (text, large text, buttons, icons and focus indicators), the places sites usually fail, how to test the hard cases like gradients and text over photos, and how to fix a failing brand color without throwing the brand away.
Understanding contrast ratios
WCAG measures color contrast as a ratio between the relative luminance of the lighter color and the darker one. The scale runs from 1:1 (the same color on itself) to 21:1 (black on white). You never need to do the math by hand, but two rules from the W3C are worth knowing:
- The thresholds are absolute. The Understanding document for 1.4.3 says computed values should not be rounded, so 4.499:1 does not meet 4.5:1.
- Measure the colors in the code, not the pixels on screen. Anti-aliasing can make thin fonts look fainter than their CSS color, but the criterion is evaluated against the foreground and background colors from the stylesheet. Very thin fonts are still worth a real look, because they read worse than their ratio suggests.
The WCAG contrast thresholds at a glance
Three success criteria set contrast ratios, and one more covers links that are told apart from text only by color:
WhatCriterionLevelMinimum ratio
Normal text1.4.3 Contrast (Minimum)AA4.5:1
Large text1.4.3 Contrast (Minimum)AA3:1
Normal text1.4.6 Contrast (Enhanced)AAA7:1
Large text1.4.6 Contrast (Enhanced)AAA4.5:1
UI component boundaries, states, focus indicators, meaningful icons and chart parts1.4.11 Non-text ContrastAA3:1 against adjacent colors
Inline links told apart from body text only by color1.4.1 Use of Color (technique G183)A3:1 against the surrounding text, plus a non-color cue on hover and focus
Why 4.5:1 and 7:1
The numbers are not arbitrary. The W3C chose 4.5:1 for AA because it compensates for the loss in contrast sensitivity usually experienced by people with vision loss equivalent to about 20/40, and 7:1 for AAA because someone with about 20/80 acuity needs roughly that much (Understanding 1.4.6). That is a useful sentence for a client who thinks the rule is a matter of taste.
Level AA is the usual target for a project and the level the rest of this guide assumes. Level AAA is rarely required for a whole site, but meeting 7:1 for body text where you can makes long reading easier for everyone.
What counts as "large text"?
WCAG defines large scale text as at least 18 point, or at least 14 point bold. The Understanding document gives the CSS equivalents as approximately 24px and 18.5px bold (14pt works out to about 18.67px, so treat 19px bold as the safe line). Large text gets the lower threshold because its size makes it easier to read.
Be careful with this one: 18px, a common body size in modern design, is not large text. A 20px subheading at regular weight is not large text either. It needs 4.5:1.
What is exempt
- Logotypes. Text that is part of a logo or brand name has no contrast requirement.
- Incidental text. Pure decoration, text nobody can see, text inside a photo that happens to contain words, and text in an inactive (disabled) control.
- Inactive controls and browser defaults under 1.4.11. A disabled button and a control whose look is set by the browser and not changed by the author are exempt from non-text contrast (Understanding 1.4.11).
The logo exemption covers the logo, not the brand color everywhere else. A pale brand color used for body text, buttons or links is held to the normal thresholds.
Non-text contrast: buttons, inputs, icons and focus
1.4.11 is the criterion teams forget. It asks for 3:1 between a component's visual cues and the colors next to them. In practice that means:
- Input borders. If the border is the only thing that shows where a text field is, it needs 3:1 against the page.
- Buttons drawn only by their background. A light gray button on a white page with no border fails if the fill is under 3:1 against white. Its label still needs 4.5:1 against the fill.
- States. The checked mark of a checkbox, the selected tab indicator and the focus ring all need 3:1 against what is around them.
- Meaningful icons and chart parts. An icon-only button, or the line in a chart that carries the data, needs 3:1. Decorative graphics do not.
/* Broken: #E5E7EB on white is 1.24:1, so the field is nearly invisible */
.input {
border: 1px solid #e5e7eb;
}
/* Fixed: #949494 on white is 3.03:1 */
.input {
border: 1px solid #949494;
}
/* Fixed: a focus ring with 3:1 against the white page */
.input:focus-visible {
outline: 2px solid #2a6ab5;
outline-offset: 2px;
}
Hover effects are an exception worth knowing: the pointer already tells the user where they are, so a subtle hover color is not held to 3:1 on its own.
Common contrast problem areas
Certain design patterns consistently fail. Knowing where to look saves time during an audit.
Light gray text on white
This is the most common offender. Designers use light gray for "secondary" text, such as timestamps, captions and metadata, to create visual hierarchy, and the result often fails.
The lightest gray that passes 4.5:1 against pure white (#FFFFFF) is #767676, at 4.54:1. Anything lighter fails. The popular #999999 only achieves 2.85:1. For AAA body text on white, you need #595959 or darker (7.0:1).
Placeholder text in forms
Placeholder text is held to 4.5:1 like any other text, and it is easy to get wrong because the input background changes the result. A placeholder of #757575 passes on a white input at 4.61:1, but on a light gray #F3F4F6 input the same color drops to 4.19:1 and fails. Placeholder text should never replace a label, but when you use it, it still needs to be readable.
Colored buttons and links
Brand-colored buttons often fail when light brand colors are used with white text, or dark brand colors with dark text. White text on a medium blue like #4A90D9 looks fine to most people and measures 3.34:1, which fails for a normal-size button label.
Links have a second test. A link inside a paragraph must be distinguishable from the text around it. If it is told apart only by color, the link color needs 3:1 against the body text color (technique G183), and that is hard to reach with dark body text. A blue of #2A6AB5 passes against white at 5.49:1, but against #333333 body text it is only 2.3:1. The simple fix is to keep the underline.
/* Broken: color alone separates the link from #333 body text (2.3:1) */
.prose a {
color: #2a6ab5;
text-decoration: none;
}
/* Fixed: the underline carries the difference, color is a bonus */
.prose a {
color: #2a6ab5;
text-decoration: underline;
text-underline-offset: 0.15em;
}
Disabled and inactive states
WCAG exempts inactive components from contrast requirements, so disabled buttons and inputs do not need 4.5:1. Users still need to perceive that a control exists and why they cannot use it, so do not fade a disabled state into the background. Pair it with a short reason ("Fill in your email to continue") where you can.
How to test gradients and text over images
Text on a flat color is arithmetic. Text over a gradient or a photo is where tools and people get it wrong, because there is no single background color.
Gradients
Measure the text against the part of the gradient behind it that is closest to the text color. For white text, that is the lightest stop the text sits on. Take a hero that fades from #2A6AB5 to #4A90D9 with white text across it:
- White on
#2A6AB5is 5.49:1, which passes for any text size. - White on
#4A90D9is 3.34:1, which passes only for large text.
So a 48px headline across the whole gradient passes AA, and the 16px paragraph under it passes only if it stays over the darker end. The fix is either to move the small text, or to end the gradient on a darker stop.
Text over photos
The W3C failure technique F83 describes the test. First, a quick check: compare the text with the lightest area of the image behind it (for light text, the darkest area). If that passes, you are done. If it does not, check the background behind each letter. Then repeat at every breakpoint, because a responsive crop moves the text onto a different part of the image.
The robust fix is to stop depending on the photo:
/* Broken: white text straight on a photo, contrast depends on the crop */
.hero {
background: url("team.jpg") center / cover;
color: #fff;
}
/* Fixed: a dark overlay sets a known floor under the text */
.hero {
background:
linear-gradient(rgb(0 0 0 / 0.55), rgb(0 0 0 / 0.55)),
url("team.jpg") center / cover;
color: #fff;
}
Check the result against the lightest part of the photo after the overlay, not before. A solid panel behind the text works too. A text shadow can help: the Understanding document treats a wide border that fills the letter's surroundings as part of the background, so a strong, wide halo can raise the measured contrast, but a soft drop shadow alone usually does not.
Fixing a failing brand color: a worked example
A common concern from design teams is that accessibility will force them to abandon their brand colors. In practice, this is rarely the case.
Darken, don't replace
Say the brand blue is #4A90D9. White button text on it is 3.34:1, so normal-size labels fail. Shifting to #2A6AB5 keeps the same hue and reaches 5.49:1, which passes AA. Going further to #1F5799 reaches 7.29:1 and passes AAA. Most visitors will not notice the change. Visitors with low vision will notice the improvement.
Give each shade a job
Keep the original color where the rules allow it: large headings (3:1), decorative shapes and backgrounds behind dark text, illustrations, and the logo. Use the darker variant for body text, button fills with white labels, links and form controls. Put both in your design tokens so nobody has to remember which blue passes.
:root {
--brand-blue: #4a90d9; /* large text, decoration, illustration */
--brand-blue-text: #2a6ab5; /* text, links and buttons on white (5.49:1) */
--brand-blue-strong: #1f5799; /* AAA body text on white (7.29:1) */
}
What to tell a client who will not change the palette
Some clients will push back. Words that tend to work:
- "We are not changing the brand. The logo is exempt, and we keep your blue for headlines and graphics. We are adding one darker shade of the same blue for small text and buttons."
- "The rule is based on how people with common vision loss read, not on taste. Roughly 20/40 vision is the reference point for the standard we are meeting."
- "Low contrast text is the most common failure the WebAIM Million found on home pages this year, so it is also what anyone testing the site will see first."
Write down what they decide. If a client keeps a failing color after you have explained it, the report should say so, rather than your team quietly carrying it.
Tools for checking contrast
You don't need to calculate contrast ratios by hand. These tools make checking quick:
- Browser DevTools. Chrome, Firefox, and Edge all show contrast ratios when you inspect text elements. Chrome's color picker also suggests colors that would meet AA and AAA thresholds.
- WebAIM Contrast Checker. Enter foreground and background hex codes and see the resulting ratio.
- Stark. A plugin that checks contrast inside your design tool.
- Operating system tools. macOS includes Digital Color Meter for sampling a pixel, which helps with text over images. On Windows, Microsoft's Accessibility Insights includes contrast checks.
How AccessGuard helps with color contrast
Checking individual color pairs works well during design, but a live site has hundreds of text elements across many templates. AccessGuard runs automated WCAG 2.2 AA checks in a real browser and flags text that falls below the threshold for its size, and links that differ from the text around them only by color.
Each contrast issue comes with the two colors found, the calculated ratio, the ratio required at that text size, and the WCAG criterion it fails, so your design team gets the exact pair to change rather than a page to go hunting through. Reports can go out as white-label PDFs under your own name.
Contrast is one of the failures automated checks are genuinely good at, because the ratio is arithmetic. Text over photographs and gradients is the exception: a tool can only measure against the colors it can read, so those still need a person using the F83 test above. Automated checks find many problems but cannot confirm conformance.
Contrast checklist
- Body text is at least 4.5:1, and 7:1 where you can manage it.
- Only text at 24px, or about 19px bold, uses the 3:1 large text threshold.
- Placeholder text is tested on the actual input background.
- Button labels pass against the button fill, and the button or field edge passes 3:1 against the page.
- Focus indicators, checkbox marks and selected states are at least 3:1 against their surroundings.
- Inline links are underlined, or reach 3:1 against the body text and gain a cue on hover and focus.
- Text over gradients is measured against the lightest stop behind it (for light text, the darkest).
- Text over photos is checked with the F83 test at every breakpoint you support.
- Accessible brand variants live in design tokens.
- Live templates are rescanned after each release, because a CSS change can undo a fix quietly.
See what a scan reports for one of your pages with the color contrast checker, or run a free scan. For the wider audit, read how to audit a website for accessibility and the most common accessibility issues and how to fix them.
Top comments (0)