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.3AAEN 301 549Section 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
Measure each text-and-background pair with a contrast checker (browser
DevTools shows the ratio inline) rather than trusting how it looks.
Hit at least 4.5:1 for normal text and 3:1
for large text (≥24px, or ≥18.66px bold).
Darken the text or lighten the background until it passes; you rarely need
to change the hue, only its luminance.
Remember placeholder text, disabled-looking captions, and text over images
or gradients — they’re the pairs that quietly fail.
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.1AEN 301 549Section 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
For every place color signals something, add a second cue: text, an icon,
a shape, or an underline.
Mark required fields with the word “(required)”, not just a red asterisk —
and if you keep the asterisk, explain it in text.
Show errors with a written message and an icon, not only a red outline; the
color reinforces, it doesn’t carry, the meaning.
For statuses, charts, and tags, combine color with labels, icons, or
patterns so they read in greyscale.
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.11AAEN 301 549ADA 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.
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
Give every control boundary you need to see — input borders, button edges,
toggle tracks — at least 3:1 against its surroundings.
Make the focus ring clear the same 3:1 bar, and add
outline-offset so it doesn’t merge into the control.
Hold meaningful icons and graphics to 3:1 too; only purely decorative
shapes (marked aria-hidden) are exempt.
Check the contrast of adjacent colors, not just against the page —
a thumb against its track, a bar against its neighbour.
Re-check states: hover, active, disabled, and the dark theme can each drop a
component below 3:1.
Links not distinguishable without color
WCAG 2.2 · 1.4.1AEN 301 549Section 508
A link sitting inside a paragraph is a special case of
the “color alone” rule. If the only thing that sets the link apart from the
surrounding text is its color, then a color-blind reader — or anyone whose screen
flattens the hue — can’t tell which words are clickable. WCAG accepts color as the
differentiator only when the link text also meets a 3:1 contrast against
the body text around it and something extra (like an underline) appears on
hover and focus. The simplest, most robust answer is to keep links underlined in
body copy so they’re distinguishable by shape, not just color.
Bad
The underline is removed and the link is only a slightly different color from
the body text. In greyscale the two are indistinguishable, so the reader can’t
find the link (1.4.1).
bad-links.css
p { color: #333; }
p a { color: #1a73e8; text-decoration: none; } /* color is the only cue */
/* In greyscale the link is the same as the text → fails 1.4.1 */
Good
The link is underlined in running text, so it’s identifiable by shape
regardless of color. Hover and focus thicken the underline to confirm the
interaction — a non-color change of state.
good-links.css
p a {
color: #1a5fff; /* still ≥ 4.5:1 vs the page for text contrast */
text-decoration: underline; /* the non-color cue: shape */
text-underline-offset: 2px;
}
p a:hover,
p a:focus-visible {
text-decoration-thickness: 2px; /* state change without relying on color */
}
Code
If a visual design truly forbids underlines in body text, you may drop them
only when the link color contrasts ≥3:1 with the surrounding text and
an underline (or equivalent) returns on hover and focus. Underlining is simpler
and safer.
links-without-underline.css
/* Allowed ONLY if both conditions hold: */
p { color: #333; }
p a {
color: #0b5bd3; /* ≥ 3:1 vs the #333 body text AND ≥ 4.5:1 vs page */
text-decoration: none;
}
p a:hover,
p a:focus-visible { text-decoration: underline; } /* extra cue on interaction */
/* Navigation menus and buttons are exempt — context already marks them as links. */
How to fix
Underline links inside body text — it’s the clearest non-color cue and
meets 1.4.1 outright.
If you must remove the underline, ensure the link color is at least
3:1 against the surrounding text and bring an
underline back on hover and focus.
Keep the link text itself at 4.5:1 against the page so it
also passes 1.4.3.
Don’t rely on underline-on-hover alone — touch and keyboard users may never
hover, so the cue must be present at rest.
Navigation bars, buttons, and obvious link lists are exempt; the rule
targets links embedded in blocks of text.
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.