Colour Rule

Text must have enough contrast with its background color-contrast

Text needs enough difference in brightness from its background to be readable by people with low vision, or by anyone on a dim screen. For normal text the ratio must be at least 4.5 to 1. For large text (about 24px, or 19px bold) it must be at least 3 to 1. Pale grey on white, and white on a light brand blue, usually fail.

Websight reports this as Elements must meet minimum color contrast ratio thresholds

Who it affects

Someone with low vision checking a gym's class timetable on their phone cannot read the pale grey room names and times, and turns up at the wrong studio.

Anyone outdoors in bright sunlight watches a white button on a light blue background disappear into the glare.

Someone in their seventies with the normal yellowing of the eye's lens finds light grey text on white far harder to read than it looked on the designer's monitor.

Broken versus fixed

Both examples are live and rendered in a sandboxed frame. The broken one is deliberately broken.

Broken

Room names in #9a9a9a on white measure 2.8 to 1. White button text on #5aa0e6 measures 2.8 to 1. Both fail the 4.5 to 1 minimum.

Fixed

Text in #595959 on white measures 7.0 to 1. White on #1b5e9e measures 6.7 to 1. Both pass with room to spare.

Show the markup behind both examples
Broken
<style>body{font-family:system-ui;margin:0;padding:16px;background:#fff}table{border-collapse:collapse;font-size:15px}td{padding:4px 12px 4px 0;color:#9a9a9a}td:first-child{color:#1f2937;font-weight:600}.btn{display:inline-block;margin-top:10px;background:#5aa0e6;color:#fff;padding:8px 14px;border-radius:6px;font-size:15px}</style>
<table>
  <tr><td>Spin</td><td>Studio 2</td><td>6.00pm</td></tr>
  <tr><td>Yoga</td><td>Studio 1</td><td>7.00pm</td></tr>
  <tr><td>Circuits</td><td>Main hall</td><td>7.30pm</td></tr>
</table>
<span class="btn">Book a class</span>
Fixed
<style>body{font-family:system-ui;margin:0;padding:16px;background:#fff}table{border-collapse:collapse;font-size:15px}td{padding:4px 12px 4px 0;color:#595959}td:first-child{color:#1f2937;font-weight:600}.btn{display:inline-block;margin-top:10px;background:#1b5e9e;color:#fff;padding:8px 14px;border-radius:6px;font-size:15px}</style>
<table>
  <tr><td>Spin</td><td>Studio 2</td><td>6.00pm</td></tr>
  <tr><td>Yoga</td><td>Studio 1</td><td>7.00pm</td></tr>
  <tr><td>Circuits</td><td>Main hall</td><td>7.30pm</td></tr>
</table>
<span class="btn">Book a class</span>

Test it by hand in two minutes

  1. Right click the text, choose Inspect, and in the Styles panel click the colour swatch next to color. Chrome shows the contrast ratio with a tick or a cross for AA.
  2. Note the font size. Below about 24px (or 19px bold) the target is 4.5 to 1; at or above that size, 3 to 1.
  3. Check text over photos, gradients and hover states too, where the background is not one flat colour.
  4. Turn the screen brightness down and look again. If you are squinting, so are your readers.

The fix

Before
.timetable td { color: #9a9a9a; }
After
.timetable td { color: #595959; }

Darken the text or lighten the background until the ratio clears the target. The hue can stay if the brand matters; it is the brightness that counts.

Button text on a brand colour

Before
.btn { background: #5aa0e6; color: #ffffff; }
After
.btn { background: #1b5e9e; color: #ffffff; }

White on the light brand blue was 2.8 to 1. The darker blue gives 6.7 to 1. A dark text colour on the light blue is the other option.

Placeholder text

Before
input::placeholder { color: #a3a3a3; }
After
input::placeholder { color: #767676; }

Placeholders count as text: #a3a3a3 on white is 2.5 to 1, #767676 is 4.5 to 1. Better still, put the hint in a visible label.

Text over a photo

Before
.hero h1 { color: #ffffff; }
After
.hero h1 { color: #ffffff; background: rgba(0, 0, 0, 0.6); padding: 4px 8px; }

A solid or dark translucent band behind the text makes the ratio predictable whatever the photo shows.

Common mistakes

  • Checking the ratio in the design file, then shipping a lighter 'muted' grey for secondary text.
  • Counting 16px semi-bold text as large; large means about 24px, or 19px bold.
  • Fading text with opacity, which lowers the contrast by an amount nobody has measured.
  • Fixing the body text and forgetting links, placeholders, buttons that look disabled but are not, and error messages in red.
  • Relying on a colour blindness simulator; contrast is about brightness, and a simulator does not measure it.

WCAG criteria this rule maps to

axe-core tags this rule as WCAG 2.2 level AA. It is one of the checks for:

The scanner works out the ratio between the text colour and the background it can resolve, allowing for font size and weight. It skips text over images, gradients and some overlapping layers and reports those for a manual check, so a clean scan does not mean every piece of text passes. Automated tools find only part of the accessibility problems on a page; a pass on this rule is not a pass on the criterion.

Scan your own page for this and 63 other checks

Open any page in Chrome, click Websight, and get a score out of 100 with every issue explained and a draft fix snippet per element.

Add Websight to Chrome

Free scans, no account, nothing leaves your computer. Automated tools find only part of the problems; the extension says so on every report.