Building an accessible colour palette

A colour palette is a set of decisions about contrast and meaning, not just a mood board. Before a colour ships, two questions decide whether everyone can use the interface: is there enough luminance contrast between this colour and what sits behind it, and does any information depend on a person being able to tell one hue from another? Get those wrong and the palette quietly excludes people with low vision, with colour-vision deficiencies — roughly one in twelve men — or anyone reading on a dim screen in bright sun.

This lesson works through the three palette defects behind most real-world colour failures: a brand colour pressed into service as body text below the required ratio, success and error states told apart by hue alone, and placeholder or disabled text dialled so faint it can’t be read. Each is checked the same way — measure the ratio, and make sure colour is never the only signal.

What you’ll learn

How to check luminance contrast and hit the 4.5:1 ratio for normal text (3:1 for large text and for UI components and graphics); why a brand colour that looks fine in a logo often fails as body copy; how to make success, warning, and error states carry an icon, label, or shape so they don’t rely on colour alone; and how to keep placeholder and disabled text legible while still signalling their state.

Standards this lesson maps to
Standard Criterion Level What it requires
WCAG 2.2 1.4.1 Use of Color A Colour is never the only visual means of conveying information, indicating an action, or distinguishing an element.
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.
WCAG 2.2 1.4.11 Non-text Contrast AA UI components and meaningful graphics have a contrast ratio of at least 3:1 against adjacent colours.
WCAG 2.2 1.4.6 Contrast (Enhanced) AAA Text reaches 7:1 (4.5:1 for large text) — a stronger target worth designing toward.
EN 301 549 9.1.4.1 / 9.1.4.3 (incorporates WCAG) European harmonised standard; references the WCAG A/AA set including the colour criteria.
Section 508 502.3 / 504 (incorporates WCAG A & AA) US federal ICT must meet WCAG 2.0 Level A and AA, including use of colour and contrast.
ADA Title II WCAG 2.1 AA (DOJ rule) AA US state/local government web content must conform to WCAG 2.1 AA.

The three problems we’ll fix

Each card below isolates one common palette 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 harm this page), a Good example, the copyable Code, and an ordered fix.

Brand colour used as body text below 4.5:1

WCAG 2.2 · 1.4.3 AA EN 301 549 Section 508 ADA Title II

Brand colours are chosen to look good as big shapes — a logo, a button fill, a hero band — where contrast against a white page is rarely the constraint. The trouble starts when that same hue is reused for running text. A mid-tone teal or sky blue that pops in a logo often lands around 3:1 or lower against white, well under the 4.5:1 that 1.4.3 requires for normal text. For example #5B9BD5 on white is only about 2.6:1 — readable for a logo, a fail for a paragraph. Large text (at least 24px, or 18.66px bold) has a lower bar of 3:1, but body copy almost never qualifies.

Bad

The brand teal is applied directly to paragraph text. It clears contrast as a logo but sits near 2.6:1 on white, far below the 4.5:1 minimum (1.4.3).

bad-brand-text.css
/* Brand hue reused for body copy — ~2.6:1 on white */
body { background: #ffffff; }
p    { color: #5b9bd5; }   /* brand teal */

Good

The palette keeps the brand hue for accents and large shapes, but defines a separate, darker text colour that clears 4.5:1. Here #1f4e79 on white is about 8.6:1 — comfortably AA, and even AAA.

good-text-shade.css
:root {
  --brand:      #5b9bd5;  /* accents, large shapes only */
  --brand-text: #1f4e79;  /* derived dark shade — ~8.6:1 on white */
}
body { background: #ffffff; }
p    { color: var(--brand-text); }

Code

Large text gets a lower 3:1 bar, so a brand hue can sometimes serve a big heading. Define both thresholds as tokens and apply them by role, never by eyeballing the colour.

contrast-tokens.css
/* Normal text: needs >= 4.5:1 */
.text-body { color: #1f4e79; }            /* ~8.6:1 */

/* Large text (>= 24px, or >= 18.66px bold): needs >= 3:1 */
.text-hero {
  font-size: 2rem; font-weight: 700;
  color: #5b9bd5;                         /* ~2.6:1 — only legal if truly large */
}
/* Safer: use the darker shade even for large text. */

How to fix

  1. Measure the text colour against its actual background with a contrast checker; require at least 4.5:1 for normal text, 3:1 for large text.
  2. Derive a separate, darker text shade from the brand hue instead of reusing the brand colour at full strength.
  3. Store both as tokens (--brand for accents, --brand-text for copy) and apply them by role.
  4. Re-check every theme — a colour that passes on white can fail on a tinted or dark background.

Success and error states distinguished by hue alone

WCAG 2.2 · 1.4.1 A EN 301 549 Section 508 ADA Title II

Red for error and green for success is the most common shorthand in interface design — and the most common 1.4.1 failure. People with red-green colour-vision deficiency, the most prevalent type, may see those two states as nearly the same muddy tone. A user reading “your details have been saved” in green and “could not save — try again” in red has no way to tell which happened if the only difference is hue. Colour can stay; it just can’t be the only signal. Add an icon, a text label, or a shape that carries the same meaning.

Bad

The two states differ only in colour. Strip the hue — as some users effectively do — and the messages are indistinguishable (1.4.1).

bad-hue-only.html
<p style="color: green">Profile saved</p>
<p style="color: red">Profile could not be saved</p>
<!-- Identical once hue is removed; meaning is colour-only -->

Good

Each state keeps its colour but adds a redundant cue: a leading icon and an explicit word (“Success”, “Error”). The meaning survives in greyscale and is exposed to assistive tech through the text.

good-redundant-cue.html
<p class="status status--ok">
  <svg aria-hidden="true" focusable="false" width="16" height="16">…</svg>
  <strong>Success:</strong> Profile saved
</p>
<p class="status status--error">
  <svg aria-hidden="true" focusable="false" width="16" height="16">…</svg>
  <strong>Error:</strong> Profile could not be saved
</p>

Code

The same rule applies to charts and badges: pair colour with a pattern, direct label, or shape. Different icon shapes (a tick vs. a cross) are distinguishable without any colour at all.

redundant-cues.css
.status { display: flex; gap: .5rem; align-items: center; }
.status--ok    { color: #0f5132; }  /* dark green — also clears 4.5:1 */
.status--error { color: #842029; }  /* dark red   — also clears 4.5:1 */
/* Icon shape (tick vs cross) + the words "Success"/"Error"
   carry the meaning when hue is unavailable. */

How to fix

  1. For every state that uses colour, add a second cue — an icon, a text label such as “Error”, or a distinct shape.
  2. Test in greyscale (or a colour-blindness simulator): if two states look the same with hue removed, the design fails 1.4.1.
  3. Don’t rely on the icon alone either — give it real text so screen readers announce the state.
  4. Make sure the state colours themselves still meet 4.5:1 for the text they colour (1.4.3).

Placeholder and disabled text too faint to read

WCAG 2.2 · 1.4.3 AA 1.4.1 A EN 301 549 Section 508

Default browser placeholder text is a pale grey near #a9a9a9, which lands around 2.5:1 on white — well below 4.5:1. Designers often push it even lighter to look “subtle”. Placeholder text that conveys real information (a hint, an example, a format) is content and must meet contrast. Disabled controls are exempt from contrast under 1.4.3, but a control greyed so far it’s invisible still creates real problems: users can’t read what the option is, and if “disabled” is signalled by lightness alone it can clash with 1.4.1. Aim for legible-but-muted, not faint.

Bad

Placeholder text is dialled down to a pale grey that fails contrast, and the disabled label is signalled by lightness alone (1.4.3, 1.4.1).

bad-faint.css
input::placeholder { color: #cfcfcf; }   /* ~1.6:1 on white — unreadable */
.option--disabled  { color: #d0d0d0; }   /* "disabled" shown by lightness only */

Good

The placeholder uses a darker grey that clears 4.5:1 while still reading as secondary. Disabled state keeps adequate contrast and is also signalled by the native disabled attribute and a label, not colour alone.

good-muted.html
<style>
  input::placeholder { color: #6c757d; } /* ~4.7:1 on white — legible, muted */
</style>
<label for="plan">Plan</label>
<select id="plan">
  <option>Starter</option>
  <option disabled>Enterprise (contact sales)</option>
</select>

Code

Treat “muted” as a contrast-checked token, not an arbitrary pale grey. The same token works for hint text, captions, and placeholders.

muted-token.css
:root {
  --text:        #1f2937;  /* ~13:1  primary copy */
  --text-muted:  #6c757d;  /* ~4.7:1 hints, captions, placeholders */
}
.hint, input::placeholder, figcaption { color: var(--text-muted); }
/* Disabled: use the native attribute; reinforce with text, not just a pale tint. */

How to fix

  1. Set placeholder and hint text to a muted colour that still clears 4.5:1 (around #6c757d on white), not the pale browser default.
  2. Never put essential instructions in placeholder text — it disappears on input; keep them in a visible label or hint.
  3. For disabled controls, rely on the native disabled attribute and a clear label, so the state isn’t conveyed by colour alone (1.4.1).
  4. Re-check the muted token on every background and theme — secondary text is the first thing to fail on tinted panels.

Recap

  • Measure contrast before you ship a colour. Normal text needs at least 4.5:1; large text (≈24px, or 18.66px bold) and UI components and meaningful graphics need 3:1 (1.4.3, 1.4.11).
  • A brand colour tuned for a logo is rarely dark enough for body copy — derive a separate, darker text shade rather than reusing the brand hue at full strength (1.4.3).
  • Never let hue be the only signal. Pair every success, warning, or error colour with an icon, a text label, or a shape so colour-blind and low-vision users get the message too (1.4.1).
  • Keep placeholder and disabled text legible. Dim it with a darker grey that still clears 4.5:1, and signal “disabled” with more than colour (1.4.3, 1.4.1).

The same fixes satisfy WCAG, EN 301 549, Section 508, and ADA Title II at once — design the palette around contrast and redundant cues and you meet them all.