# Status dots told apart by colour alone

Source: https://easeweb.dev/learn/status-colours
Topics: Colour and contrast > Status, selection and charts are shown by more than colour (WCAG 1.4.1)
The fix: Either write each status as a word beside its dot, in the text colour, so the colour only adds to it; or give each status a shape of its own, such as a tick, a truck and a warning sign, with its word kept beside it in the key. The word works on its own; the shape passes 1.4.1 but still has to be matched to the key.
Test with: colour vision, contrast measurement, zoom, forced colors
References: https://www.w3.org/WAI/WCAG22/Understanding/use-of-color.html https://www.w3.org/WAI/WCAG22/Techniques/general/G14 https://www.w3.org/WAI/WCAG22/Techniques/general/G111 https://developer.chrome.com/docs/devtools/rendering/apply-effects#emulate_vision_deficiencies

This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.

## What happens

A shop's account page lists your orders: the order number, the date, the total and, at the end of each row, a small coloured dot. Green is Delivered, amber is On its way and red is Payment failed, and a key above the list says so. The design keeps each row short enough to fit a phone on one line, and a dot reads at a glance where a word would wrap. The developer built it with care: each dot carries visually hidden text, so a screen reader says "Payment failed" at the end of the row.

Now look at the list as someone with deuteranopia, a red–green colour vision deficiency, sees it. The green, the amber and the red turn into nearly the same olive. The key still says which colour means what, but its three dots look alike as well. Order 1044's payment has failed and the shop is waiting for another card, but the person it belongs to sees four orders that look the same, and nothing on the screen tells them which one needs them.

## Why it fails

WCAG 1.4.1 Use of Color says colour must not be the only visual means of conveying information, and an order's status is information. The dots differ from one another in colour and in nothing else: the same size, the same round shape, the same place in the row. The key doesn't help, because it is matched to the rows by the same colours. Nor does the hidden text: the Understanding page for 1.4.1 says that information passed to assistive technology in the code can still fail the criterion, since it asks for a visible alternative to colour.

Understanding 1.4.1 allows one way for colour to carry the difference: two colours that also differ in lightness, by a contrast ratio of at least 3:1 between them, count as a second visual cue. These three are far from it. Against each other, the green `#2f9e44`, the amber `#b7791f` and the red `#e03131` reach only 1.06:1 to 1.31:1, so they differ almost entirely in hue, and hue is what a colour vision deficiency takes away. No set of three status dots on a white page can meet that exception: a dot with no word beside it needs 3:1 against white under WCAG 1.4.11 Non-text Contrast, and that leaves room for two colours 3:1 apart, never three.

## Who is affected

The same happens on a phone in sunlight or a dimmed laptop screen, where the three colours fade into one another. Screen reader users are not shut out here: the hidden text tells them each status. The failure is for people who look at the list.

## What automation and AI miss

Nothing in the markup is wrong. Each dot is an empty span with a hidden name, and a scan that checks names passes it. axe checks colour used alone only for links in a block of text, and none of its rules compares the dots in a list. The audit can find small coloured markers that carry no visible words and come in two or more colours in one list or table, but whether the colours mean something takes a person to decide, so they go to review. When asked to fix it, AI coding tools tend to add a `title` or an `aria-label` to the dot, which adds text that nobody sees (a title shows only to a mouse resting on a 10-pixel dot), or swap in a palette sold as safe for colour blindness, which helps some people tell the hues apart and still leaves colour as the only cue.

## Before: a status shown by the dot's colour, named only for screen readers

```html
<h2>Your orders</h2>
<ul class="legend">
  <li>
    <span class="status status-delivered"></span>
    Delivered
  </li>
  <li>
    <span class="status status-shipped"></span>
    On its way
  </li>
  <li>
    <span class="status status-failed"></span>
    Payment failed
  </li>
</ul>
<ol class="orders">
  <li>
    <a href="/orders/1044">Order 1044</a>
    <span>15 Oct</span>
    <span>$64.00</span>
    <span class="status status-failed"><span class="visually-hidden">Payment failed</span></span>
  </li>
  …
</ol>
```

```css
.status {
  display: inline-block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
}
.status-delivered {
  background: #2f9e44; /* 3.45:1 on white */
}
.status-shipped {
  background: #b7791f; /* 3.64:1 on white */
}
.status-failed {
  background: #e03131; /* 4.51:1 on white */
}
```

A screen reader hears each status, but on screen the dots differ by colour alone, and the key above can be matched to them only by the same colour.

## After: the status written beside the dot

```html
<h2>Your orders</h2>
<ol class="orders">
  <li>
    <a href="/orders/1044">Order 1044</a>
    <span>15 Oct</span>
    <span>$64.00</span>
    <span class="status status-failed">Payment failed</span>
  </li>
  …
</ol>
```

```css
/* The word, in the text colour, with the dot kept in front of it. */
.status {
  display: inline-flex;
  align-items: center;
  gap: 0.375rem;
}
.status::before {
  content: "";
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--dot);
}
.status-delivered {
  --dot: #2f9e44; /* 3.45:1 on white */
}
.status-shipped {
  --dot: #b7791f; /* 3.64:1 on white */
}
.status-failed {
  --dot: #e03131; /* 4.51:1 on white */
}
```

Each row now says its status in words, so the colour adds to the word instead of carrying it. The dot keeps the design's colour coding for everyone who can see it, the key above the list is no longer needed, and the hidden text goes, because the visible word is what a screen reader reads. The word is set in the text colour, not in the status colour: the amber has 3.64:1 against white, enough for a dot but short of the 4.5:1 that text needs. On a phone, the word wraps under the total rather than being dropped.

## After: a shape for each status, beside its word in the key

```html
<h2>Your orders</h2>
<ul class="legend">
  <li>
    <span class="status status-delivered"><svg aria-hidden="true"><use href="#icon-check"></use></svg></span>
    Delivered
  </li>
  <li>
    <span class="status status-shipped"><svg aria-hidden="true"><use href="#icon-truck"></use></svg></span>
    On its way
  </li>
  <li>
    <span class="status status-failed"><svg aria-hidden="true"><use href="#icon-alert"></use></svg></span>
    Payment failed
  </li>
</ul>
<ol class="orders">
  <li>
    <a href="/orders/1044">Order 1044</a>
    <span>15 Oct</span>
    <span>$64.00</span>
    <span class="status status-failed"><svg aria-hidden="true"><use href="#icon-alert"></use></svg><span class="visually-hidden">Payment failed</span></span>
  </li>
  …
</ol>
```

```css
/* A shape of its own for each status, drawn in the status colour. */
.status {
  display: inline-block;
  width: 18px;
  height: 18px;
}
.status svg {
  width: 100%;
  height: 100%;
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
}
.status-delivered {
  color: #2f9e44; /* 3.45:1 on white */
}
.status-shipped {
  color: #b7791f; /* 3.64:1 on white */
}
.status-failed {
  color: #e03131; /* 4.51:1 on white */
}
```

A tick in a circle, a truck and a warning triangle now tell the statuses apart without colour, and the row stays as short as it was drawn. WCAG's technique G111 pairs colour with a pattern in the same way. The shapes need 3:1 against the page, like the dots, and the stroke in `currentColor` keeps them in a Windows contrast theme. This passes 1.4.1 only while the key is there and its shapes match the rows: each shape still has to be looked up, which is slow on a long list, hard at 400% zoom where the key has scrolled away, and hard for anyone who doesn't remember which symbol means what.

## Which option to use

Both pass 1.4.1. Use the word: it needs no key, works at any zoom and for every reader, and costs only room for two words in each row, which the phone layout in the design file should show wrapping, not dropped. The shapes keep the short row, but they lean on the key. Choose them only where a word really can't fit, such as a dense table on an internal dashboard, and keep each shape's word in the key and in its hidden text.

## Implementation decisions

A `title` on the dot is not a second cue. It shows only to a mouse that rests on the dot, never to touch or the keyboard; [form field labels and hints only in a title tooltip](/learn/title-tooltips) shows the same limit on form fields. A soft pill that holds the word on a tint of the status colour works as well as a plain word, and the design ideas below show it and other looks.

Make the status a component that takes the word, not the colour: `<OrderStatus status="failed">` renders both the dot and "Payment failed" from one table, so no page can draw the dot without the word. The same table can serve stock levels, payment states and booking slots, where the same dots tend to appear.

In a Windows contrast theme, the browser replaces background colours with the theme's own, so the Before's dots disappear altogether. The word in the first fix stays. The shapes in the second fix stay because they are drawn as strokes in `currentColor`; an icon with a fixed fill keeps its own colour, which can be lost against the theme's background. [High contrast mode hiding focus, icons and selections](/learn/forced-colors) covers that case.

## Verify the fix

1. In Chrome DevTools, open the Rendering panel and emulate deuteranopia, then protanopia, and confirm that every row's status can be told without looking at the key.
2. Turn the page to greyscale with the operating system's colour filters, and confirm the same.
3. Measure each status colour against the background beside it, and confirm any status word reaches 4.5:1 and any icon or dot with no word beside it reaches 3:1.
4. Zoom to 400%, scroll to the last order, and confirm its status can be read while the key is out of view.
5. At a phone's width, confirm every row still shows its word or shape.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the status colours or the order list's layout change.

## Limits

This is a starting pattern, not a guarantee for every application. Colour values are examples; measure your own palette against your real backgrounds. This guide covers a status shown by a coloured marker. Colour used alone in other places fails the same criterion and has its own guides: [links in body text marked only by colour](/learn/links-in-text), [required fields marked only by colour](/learn/required-fields) and [form errors that don't say how to fix them](/learn/accessible-form-errors). An instruction that points to something by its colour, "the orders in red need your attention", is covered in [instructions that rely on colour, shape or position](/learn/sensory-instructions). A selected option shown by a fill alone, and chart lines told apart by colour, are the same failure and get their own guides later. Check the complete pages with people who rely on them.

---

Get this guide in your coding agent: MCP https://easeweb.dev/mcp (setup at https://easeweb.dev/services/developers)
