Colour, contrast and accessible visual design

Contrast ratios for text and non-text content, not relying on colour alone, focus indicator visibility, dark mode, and a palette approach that passes by construction.

The ratios that matter

ContentMinimum ratioLevelNotes
Body text4.5:1AAMeasured against the background behind the text
Large text (18.66 px bold or 24 px)3:1AALarge means a specific size, not a subjective impression
UI component boundaries3:1AAInput borders, checkbox outlines, toggles
Graphical objects3:1AAIcons that carry meaning, chart lines
Focus indicator3:1 against both statesAAThe indicator and the component it surrounds
Body text, enhanced7:1AAAAspirational for long-form reading
Placeholder text4.5:1AAIt is text; the browser default usually fails
/* a palette that passes by construction, not by inspection */
:root {
  --text:        #1a1a1a;   /* 16.1:1 on white */
  --text-muted:  #595959;   /*  7.0:1 on white - still AA for body text */
  --accent:      #14509b;   /*  8.6:1 on white, so it works as link and button text */
  --border:      #767676;   /*  4.5:1 on white - passes as a UI boundary */
  --focus:       #b34a00;   /*  distinct hue, 4.6:1 on white, never colour-only */
}

:focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;      /* keeps the indicator clear of the component edge */
}

/* never remove the outline without replacing it */
button:focus-visible { outline: 3px solid var(--focus); outline-offset: 2px; }
  • Contrast is measured against what is actually behind the text, including an image, a gradient or a translucent panel - not against the nominal page background.
  • Grey placeholder text is the most commonly failed element on forms. If the instruction matters, it belongs in a label, not a placeholder.
  • Disabled controls are exempt from the contrast requirement, but making them unreadable is still a usability problem - use opacity sparingly.

Never colour alone

<!-- a status shown with colour, an icon and text -->
<p class="status status-error">
  <svg aria-hidden="true" focusable="false" width="16" height="16"><!-- exclamation --></svg>
  <strong>Payment failed.</strong> The card was declined by the issuer.
</p>

<!-- a chart with a pattern as well as a colour, and a legend that is text -->
<figure>
  <figcaption>Monthly revenue by region, 2026</figcaption>
  <svg role="img" aria-labelledby="chart-title chart-desc">
    <title id="chart-title">Monthly revenue by region</title>
    <desc id="chart-desc">Europe rises steadily; North America is flat; Asia doubles in June.</desc>
  </svg>
</figure>
Meaning conveyedColour-only failureAdd
Error stateA red borderAn icon, a message, and aria-invalid
Required fieldA red asteriskThe word "required" in the label
Chart seriesSix similar huesPatterns, direct labels, or distinct shapes
Link in body textA different colour onlyAn underline
LegendColour swatchesText labels beside each swatch
ProgressGreen to redA number or a percentage

Colour blindness affects roughly one in twelve men. It is not only red and green: deuteranopia and protanopia shift many pairs toward each other, including orange and green, and blue and purple. Testing a screenshot through a simulation takes a minute.

Dark mode and custom themes

@media (prefers-color-scheme: dark) {
  :root {
    --text:       #f2f2f2;   /* 15.4:1 on #121212 */
    --text-muted: #b8b8b8;
    --accent:     #8ab4f8;   /* lightened, because the dark blue failed on dark */
    --border:     #8c8c8c;
    --focus:      #ffb37a;
  }

  /* images designed for light backgrounds often need a tweak */
  img.illustration { filter: brightness(0.9) contrast(1.05); }
}

/* and honour a user who has asked for more contrast */
@media (prefers-contrast: more) {
  :root { --text-muted: var(--text); --border: #000; }
}
  • A palette built for light backgrounds usually fails in dark mode. Accents in particular become too dark; test every combination again rather than inverting.
  • Do not rely on the operating system to invert anything. Assume the browser will honour your colours and check them.
  • Custom themes chosen by the user need the same contrast checks as the defaults. A theme picker without a contrast constraint ships inaccessible options.
⚠️
Removing the focus outline and replacing it with a subtle background change is the single most common accessibility regression in a redesign. A focus indicator must be visible against both the component and its surroundings, and it must not be the only cue for the current item in a list or a set of tabs.

FAQ

Should I design the palette first or check it afterwards?
Design it to pass from the start. Choosing colour values that already meet the ratios avoids a late redesign, and the constraint rarely makes the result look worse.
Is a contrast checker enough?
It tells you whether a pair passes, not whether the design communicates. Pair it with a colour-blindness simulation and always add a second non-colour cue.

WCAG in plain language Images, media and alternative text

Last refreshed 2026-09-18.