Color & contrast

Color does two jobs on a page: it has to be legible, and it must never be the only way something is communicated. Most color failures come down to one of those two ideas. Text or an icon can be too pale to read against its background; or an important distinction — this field has an error, this item is done, this word is a link — is signalled by hue alone, so anyone who can’t perceive that hue is left guessing. Both problems hit the same broad audience: people with low vision, the one in twelve men who are color-blind, older users, and anyone on a dim phone in sunlight.

This lesson works through the four defects behind the majority of real-world color failures. Each fix is small, and none of them ask you to give up color — they ask you to make color readable and to back it up with a second cue.

What you’ll learn

How to measure and meet the WCAG contrast ratios for text (4.5:1 normal, 3:1 large); how to add a text, icon, or shape cue so meaning never rides on color alone; how to give buttons, inputs, icons, and focus rings at least 3:1 against their surroundings; and how to keep links distinguishable from body text without relying on color.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 1.4.3 Contrast (Minimum) AA Text and images of text have a contrast ratio of at least 4.5:1, or 3:1 for large text (≥24px, or ≥18.66px bold).
WCAG 2.2 1.4.1 Use of Color A Color is never the only visual means of conveying information, indicating an action, or distinguishing an element.
WCAG 2.2 1.4.11 Non-text Contrast AA UI components and meaningful graphics have at least 3:1 contrast against adjacent colors, including focus indicators.
WCAG 2.2 1.4.5 Images of Text AA Real text is used instead of an image of text wherever the same presentation is possible, so it can be resized and recolored.
WCAG 2.2 1.4.6 Contrast (Enhanced) AAA Optional higher bar: 7:1 for normal text and 4.5:1 for large text, useful as a target for body copy.
EN 301 549 9.1.4.3 / 9.1.4.11 / 9.1.4.1 (incorporates WCAG) European harmonised standard; references the WCAG A/AA set including the color and contrast criteria.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA, including contrast and use of color.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA, including 1.4.3, 1.4.1, and 1.4.11.

The four problems we’ll fix

Each card below isolates one common color or contrast defect. For every issue you get a plain-language statement of the problem, a Bad example (shown as escaped, non-running code so it can’t affect this page), a Good example, the copyable Code, and an ordered fix.

Text contrast too low

WCAG 2.2 · 1.4.3 AA EN 301 549 Section 508

Light grey text on white is the single most common contrast failure on the web. A color like #999 on #fff measures about 2.85:1 — well under the 4.5:1 the standard asks for normal-size text. It may look clean on a bright, calibrated design monitor, but on a dimmed laptop, an older screen, or a phone in sunlight it turns to a pale smear, and for someone with reduced contrast sensitivity it can disappear entirely. The rule is concrete and measurable: body text needs a contrast ratio of at least 4.5:1, and large text — 24px, or 18.66px (14pt) if bold — needs at least 3:1.

Bad

Grey body text set to #999 on a white background. At roughly 2.85:1 it fails 1.4.3 for normal text, and it would fail even the lower 3:1 large-text bar by a hair.

bad-text-contrast.css
.note {
  color: #999;            /* on #fff ≈ 2.85:1 — fails AA */
  background: #fff;
  font-size: 16px;
}

Good

Darkening the text to #595959 lifts it to about 7:1 on white — comfortably past AA (4.5:1) and even past the AAA bar (7:1). Same hue family, just enough luminance to be readable.

good-text-contrast.css
.note {
  color: #595959;         /* on #fff ≈ 7:1 — passes AA and AAA */
  background: #fff;
  font-size: 16px;
}

Code

Large text is allowed the lower 3:1 ratio, so a heading can use a lighter grey than body copy. Define both as tokens and check each pairing with a contrast tool rather than eyeballing it.

contrast-tokens.css
:root {
  --text-body: #595959;   /* ≥ 4.5:1 on white — normal text */
  --text-muted: #767676;  /* ≈ 4.54:1 — the lightest grey that still passes AA */
}
/* Large text (≥24px, or ≥18.66px bold) only needs 3:1,
   so a heading may use a lighter grey than body copy. */
h2 { color: #767676; font-size: 28px; }   /* large → 3:1 is enough */
p  { color: var(--text-body); }            /* normal → needs 4.5:1 */

How to fix

  1. Measure each text-and-background pair with a contrast checker (browser DevTools shows the ratio inline) rather than trusting how it looks.
  2. Hit at least 4.5:1 for normal text and 3:1 for large text (≥24px, or ≥18.66px bold).
  3. Darken the text or lighten the background until it passes; you rarely need to change the hue, only its luminance.
  4. Remember placeholder text, disabled-looking captions, and text over images or gradients — they’re the pairs that quietly fail.
  5. Check the dark theme too: a color that passes on white can fail on a dark surface, and vice versa.

Meaning carried by color alone

WCAG 2.2 · 1.4.1 A EN 301 549 Section 508

Use of Color is a Level A rule — the strictest tier — and it’s broken constantly: a field that turns red to show an error, a required field marked only by a red asterisk, “the items in green are complete”, a status dot that’s the only difference between “online” and “offline”. To anyone who can’t perceive the hue — a color-blind user, someone on a greyscale or high-contrast display, or a person whose screen washes the color out — the distinction simply isn’t there. The fix is never to remove the color; it’s to add a second cue: text, an icon, a shape, or an underline that carries the same meaning.

Bad

The only signal that this field is invalid, or that it’s required, is color: a red border and a red asterisk. Remove the hue and there is nothing left to read.

bad-color-only.html
<!-- Required shown only by a red asterisk’s color -->
<label>Email <span style="color:red">*</span></label>
<input class="invalid">   <!-- .invalid only sets a red border -->

<p>Items shown in green are complete.</p>

Good

Every distinction now has a non-color cue too: the word “(required)”, a visible error message with an icon, and a “Done” label beside the color. The color stays — it’s just no longer doing the job alone.

good-not-color-only.html
<label>Email <span class="req">(required)</span></label>
<input aria-invalid="true" aria-describedby="email-err">
<p id="email-err" class="error">
  <svg aria-hidden="true"><!-- warning icon --></svg>
  Enter a valid email address.
</p>

<p><span class="badge badge--done">✓ Done</span> — complete items are labelled, not just green.</p>

Code

For status, charts, and tags, pair the color with a shape, pattern, or text label so the meaning survives in greyscale. A quick test: turn the design to greyscale — if you can still tell the states apart, you’ve met 1.4.1.

status-with-second-cue.html
<!-- Color + icon + text, not color alone -->
<span class="status status--ok">● Online</span>
<span class="status status--off">▲ Offline</span>

<!-- Chart: differentiate series by pattern as well as color -->
<rect fill="url(#hatch)"></rect>   <!-- series A: hatched -->
<rect fill="url(#dots)"></rect>    <!-- series B: dotted -->

How to fix

  1. For every place color signals something, add a second cue: text, an icon, a shape, or an underline.
  2. Mark required fields with the word “(required)”, not just a red asterisk — and if you keep the asterisk, explain it in text.
  3. Show errors with a written message and an icon, not only a red outline; the color reinforces, it doesn’t carry, the meaning.
  4. For statuses, charts, and tags, combine color with labels, icons, or patterns so they read in greyscale.
  5. Sanity-check by viewing the page in greyscale or a color-blindness simulator — if a distinction vanishes, it failed 1.4.1.

Non-text and UI contrast too low

WCAG 2.2 · 1.4.11 AA EN 301 549 ADA Title II

Contrast isn’t only about text. Non-text Contrast asks that the parts of a control you need to see to use it — a button’s edge, an input’s border, the bars of a toggle, a meaningful icon, and the focus ring — stand out at least 3:1 against whatever is next to them. A pale grey input outline (say #ddd on white, about 1.3:1) leaves a low-vision user unable to tell where the field is; a focus ring that barely differs from the page makes keyboard navigation guesswork. The same 3:1 floor applies to the focus indicator, which ties this rule to keyboard accessibility.

Bad

The input border and the button outline are barely-there greys, and the focus ring is a faint tint of the background. All three sit well under 3:1, so the controls’ boundaries and focus state are invisible to many users.

bad-ui-contrast.css
input  { border: 1px solid #ddd; }   /* ≈ 1.3:1 on #fff — fails */
.btn   { border: 1px solid #e5e5e5; background:#fff; }  /* edge invisible */
:focus { outline: 2px solid #cfe2ff; }  /* ≈ 1.2:1 — focus ring too faint */

Good

The borders move to a grey that clears 3:1, and the focus ring uses a strong color with an offset so it never blends into the control. Now the edges and the focus state are unmistakable.

good-ui-contrast.css
input  { border: 1px solid #767676; }  /* ≈ 4.54:1 on #fff — passes */
.btn   { border: 1px solid #767676; background:#fff; }
:focus-visible {
  outline: 3px solid #1a5fff;          /* ≈ 4.5:1 — well over 3:1 */
  outline-offset: 2px;                 /* keeps the ring off the control edge */
}

Code

Meaningful icons and graphical objects fall under the same 3:1 rule. A decorative flourish doesn’t need to pass, but an icon a user must perceive to understand or operate the control does.

icon-and-graphic-contrast.css
/* A functional icon (e.g. a search glyph) must be ≥ 3:1 vs its background */
.icon-search { color: #595959; }       /* ≈ 7:1 on white — fine */

/* Toggle: the track and thumb must differ from each other and the page by 3:1 */
.switch__track { background: #767676; } /* off state edge visible */
.switch__thumb { background: #fff; border: 1px solid #767676; }

/* Purely decorative shapes are exempt — mark them aria-hidden and ignore 1.4.11 */

How to fix

  1. Give every control boundary you need to see — input borders, button edges, toggle tracks — at least 3:1 against its surroundings.
  2. Make the focus ring clear the same 3:1 bar, and add outline-offset so it doesn’t merge into the control.
  3. Hold meaningful icons and graphics to 3:1 too; only purely decorative shapes (marked aria-hidden) are exempt.
  4. Check the contrast of adjacent colors, not just against the page — a thumb against its track, a bar against its neighbour.
  5. Re-check states: hover, active, disabled, and the dark theme can each drop a component below 3:1.

Recap

  • Check every text color against its background: at least 4.5:1 for normal text and 3:1 for large text (1.4.3).
  • Never let color be the only cue. Pair red errors with text or an icon, mark required fields in words, and don’t say “the green ones are done” without a label or symbol (1.4.1).
  • Give buttons, inputs, icons, and the focus ring at least 3:1 against what’s next to them (1.4.11).
  • Make links stand out from body text with more than color — an underline is the safest cue (1.4.1).
  • Test in a real environment: a dimmed laptop, a phone in sunlight, and a color-blindness simulator catch what a bright design monitor hides.

These fixes satisfy WCAG, EN 301 549, Section 508, and ADA Title II at once — get the color right and you meet them all.