# Step indicators that mark the current step only by its look

Source: https://easeweb.dev/learn/form-steps
Topics: Forms > Multi-step forms say which step you are on (WCAG 1.3.1)
The fix: Say which step is current, and which are done, in the code as well as on screen. Either put aria-current="step" on the current step and visually hidden "completed" text on each finished one, and draw the look from the same markers, or write the step and its number into the step's heading, such as "Step 2 of 4: Delivery", which is enough on its own only while the steps are always taken in order.
Test with: screen reader, keyboard, forced colors
References: https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html https://www.w3.org/WAI/tutorials/forms/multi-page/ https://www.w3.org/TR/wai-aria-1.2/#aria-current https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-current https://www.w3.org/WAI/WCAG22/Techniques/general/G130

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 checkout is drawn as four steps under one heading, Checkout: Cart, Delivery, Payment and Review, each a numbered circle joined to the next by a line. The design keeps the page calm and shows progress at a glance: the step you are on is a filled blue circle with its name in bold, the finished ones have a tick, and the ones to come are grey outlines. A sighted shopper on the delivery step sees straight away that they are halfway through.

A screen reader user on the same page hears "Checkout, heading", then a list: "Cart, link", "2 Delivery", "3 Payment", "4 Review". Nothing says that Delivery is the step they are on, or that Cart is done; the tick is drawn, not written. The heading reads Checkout on every step, so it doesn't tell them either. They find out where they are by reading on to the fields, and after a page reload or a Back press they have to do it again.

## Why it fails

The indicator shows relationships: this step is the current one, these are finished, these are still to come. WCAG 1.3.1 Info and Relationships requires information shown by presentation to be available in the code, or in the text. Here it is only in the look. The fill comes from a `current` class and the tick from a `done` class, and class names mean nothing to the browser, so assistive technology has nothing to pass on. The tick itself is an icon hidden from screen readers, with no words in its place.

The heading passes. WCAG 2.4.6 Headings and Labels asks that headings describe their topic or purpose, and Checkout describes the page; it doesn't ask for a step number. Naming the step in its heading is still the plainest way to tell everyone where they are, so it is one of the fixes here: it gives the current step in text, which is what 1.3.1 needs.

The look itself passes. The current step is a filled circle with its name in bold, not a change of colour alone, so the drawing meets 1.4.1 Use of Color. Every circle's border has at least 3:1 contrast with the white page (3.03:1 for the steps still to come, 6.84:1 for the others), and the tick 5.49:1 with its pale fill, as 1.4.11 Non-text Contrast asks. Marking the step in code also meets technique ARIA26, which is sufficient for 2.4.8 Location, the Level AAA criterion about knowing where you are within a set of pages.

## Who is affected

Checkouts, sign-ups, applications and bookings split into steps all use an indicator like this, and its look varies more than its markup: circles joined by a line, numbered tabs, a row of words with chevrons. Whichever way it is drawn, if the current step is only a style, people who can't see the style lose their place.

## What automation and AI miss

A scanner sees a valid ordered list of short text and passes it. It can't tell that one item is drawn as current, because a class called `current`, `active` or `is-complete` means nothing to it, and it can't check that the mark moves when the step does. EaseWeb's audit reports a list marked as steps by its class or name in which one item has a class such as `current` or `active`, is drawn differently from the others, and has neither `aria-current` nor hidden text, when neither a heading nor the page title names the step. It sends other step lists that are drawn unevenly with nothing in the code to say why, and unmarked lists on a page whose heading or title names the step, to a person as review leads. AI code generators may reach for `aria-selected="true"` or `role="tab"` on the current step, which belong to tabs and listboxes and are ignored or misread on a list item, or turn the indicator into a `role="progressbar"` whose value of 2 can be read as "2 percent". Listening to the indicator on two different steps is the only way to confirm it says where you are.

## Before: the current step drawn by a class

```html
<h1>Checkout</h1>
<ol class="steps" role="list">
  <li class="done">
    <a href="/checkout/cart">
      <span class="mark"><svg aria-hidden="true" viewBox="0 0 16 16">…</svg></span>
      Cart
    </a>
  </li>
  <li class="current">
    <span class="mark">2</span>
    Delivery
  </li>
  <li>
    <span class="mark">3</span>
    Payment
  </li>
  <li>
    <span class="mark">4</span>
    Review
  </li>
</ol>
```

```css
.steps {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  padding: 0;
}
.steps .mark {
  border: 2px solid #8796a5;
  border-radius: 50%;
  color: #5b6670;
}
.steps .done .mark {
  border-color: #1f5f8b;
  background: #dbe8f2;
  color: #1f5f8b;
}
.steps .current .mark {
  border-color: #1f5f8b;
  background: #1f5f8b;
  color: #fff;
}
.steps .current {
  font-weight: 700;
}
```

The `current` and `done` classes give the steps their look and nothing else. A screen reader reads "2 Delivery" exactly as it reads "3 Payment", and reads the finished step as "Cart, link", because its tick is hidden and has no words.

## After: aria-current on the current step, and hidden text on the finished ones

```html
<h1>Checkout</h1>
<ol class="steps" role="list">
  <li class="done">
    <a href="/checkout/cart">
      <span class="mark"><svg aria-hidden="true" viewBox="0 0 16 16">…</svg></span>
      Cart<span class="visually-hidden">, completed</span>
    </a>
  </li>
  <li aria-current="step">
    <span class="mark">2</span>
    Delivery
  </li>
  <li>
    <span class="mark">3</span>
    Payment
  </li>
  <li>
    <span class="mark">4</span>
    Review
  </li>
</ol>
```

```css
.steps {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  padding: 0;
}
.steps .mark {
  border: 2px solid #8796a5;
  border-radius: 50%;
  color: #5b6670;
}
.steps .done .mark {
  border-color: #1f5f8b;
  background: #dbe8f2;
  color: #1f5f8b;
}
.steps [aria-current="step"] .mark {
  border-color: #1f5f8b;
  background: #1f5f8b;
  color: #fff;
}
.steps [aria-current="step"] {
  font-weight: 700;
}
.visually-hidden {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
```

`aria-current="step"` tells assistive technology that this item is the current step in a process, and screen readers that support it announce it as the current step. The style now selects on the attribute, so the look and the code come from one marker and can't point at different steps. ARIA has no state for a finished step, so the tick gets its words as hidden text inside the link: "Cart, completed, link". Write that text from the same template condition that adds the `done` class, and translate it with the rest of the page. The design is unchanged: every circle, fill and tick looks as it was drawn.

## After: the step written into its heading

```html
<h1><span class="step-count">Step 2 of 4</span> Delivery</h1>
<ol class="steps" role="list">
  <li class="done">
    <a href="/checkout/cart">
      <span class="mark"><svg aria-hidden="true" viewBox="0 0 16 16">…</svg></span>
      Cart
    </a>
  </li>
  <li class="current">
    <span class="mark">2</span>
    Delivery
  </li>
  <li>
    <span class="mark">3</span>
    Payment
  </li>
  <li>
    <span class="mark">4</span>
    Review
  </li>
</ol>
```

```css
.steps {
  display: flex;
  gap: 1.5rem;
  list-style: none;
  padding: 0;
}
.steps .mark {
  border: 2px solid #8796a5;
  border-radius: 50%;
  color: #5b6670;
}
.steps .done .mark {
  border-color: #1f5f8b;
  background: #dbe8f2;
  color: #1f5f8b;
}
.steps .current .mark {
  border-color: #1f5f8b;
  background: #1f5f8b;
  color: #fff;
}
.steps .current {
  font-weight: 700;
}
.step-count {
  display: block;
  color: #5b6670;
  font-size: 0.875rem;
  font-weight: 600;
}
```

This changes the resting look: the heading no longer reads Checkout but names the step, with a small grey line above it giving its number, so the page says "Step 2 of 4, Delivery" in words that everyone reads, whether or not they see the indicator. A screen reader reads the heading as "Step 2 of 4 Delivery", and someone moving by headings hears it first. The indicator itself is as before, so it still says nothing about which step is current or done. That is enough only while the steps are always taken in order: on step 2 of 4, step 1 must be the finished one. Where people can skip a step, or come back to one they left unfinished, the list needs its own marks as well.

## Which option to use

Both say which step you are on. Use `aria-current="step"` with hidden text on the finished steps: it fixes the indicator itself, keeps the design exactly as drawn, and draws the look from the same markers, while the heading alone passes only as long as the steps are always taken in order. The step in the heading is still worth adding beside it. It helps people who never see the indicator, such as magnifier users looking at the fields, and people who need the count in plain words, and it costs the designer only a line of small text above the heading.

## Implementation decisions

The indicator and the page around it need a few more choices:

- Lists without markers: Safari's VoiceOver can drop the list role from a list whose numbers or bullets CSS has removed, so "list, 4 items" is never said. Add `role="list"` to the `ol` to keep it.
- Finished steps as links: W3C's multi-page forms tutorial suggests linking each finished step so people can go back and check it. Keep its hidden "completed" text inside the link, so it is part of the link's name. Leave steps still to come as plain text, not links that do nothing.
- Hidden text for the current step too: where the indicator comes from a component that takes text but not attributes, write ", current step" as hidden text inside the current item. The tutorial does the same, with "Current: " before the name. Set it from the same condition as the class, and translate it.
- Single-page checkouts: when Continue swaps the step without loading a page, the router must move `aria-current` and focus with it; see [single-page app route changes that are never announced](/learn/route-changes). A step whose change is never announced leaves a screen reader user on a button that now belongs to another step.
- Page titles: putting the step in the page's title, such as "Step 2 of 4: Delivery – Checkout", tells screen reader users where they are as soon as a page loads. That is 2.4.2 Page Titled, covered in [page titles that are the same on every page](/learn/page-title).
- A bar instead of circles: a progress bar for steps needs `aria-valuetext="Step 2 of 4: Delivery"`, or its value is read as a percentage; see [progress bars and meters that screen readers can't read](/learn/progress-bars).

Keep the current step visible in forced colors. A Windows contrast theme replaces the circle's fill with its own colours, so the filled circle can look like the others; the bold name and the border survive. Draw the current step with more than a fill, such as a bold name or a thicker ring, and check it in a contrast theme. On a phone, designers often shrink the indicator to "Step 2 of 4" in words, which passes; shrinking it to dots alone does not.

## Verify the fix

1. In the code or the browser's inspector, find the current step and confirm it has `aria-current="step"` or hidden text inside it, or that the step's heading names it with its number, and that the visual style comes from the same step value.
2. With a screen reader, read the indicator from the top and confirm the current step is announced as current, or the heading says which it is, and that the finished steps are not read like the ones still to come.
3. Press Continue and Back, and then the browser's Back button, and confirm the look, the markers and the heading have all moved to the new step.
4. Arrive on a step as someone new would, by reloading it or following a link to it, and confirm the page says which step it is, and of how many, before the first field.
5. Turn on forced colors, such as a Windows contrast theme, and confirm the current step still looks different from the others.

Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the checkout's steps or its step component change.

## Limits

This is a starting pattern, not a guarantee for every process. Screen readers word `aria-current` differently, some before the name and some after, and support varies between screen readers and browsers; test with the ones your visitors use. An indicator that marks the right step still leaves people lost in a process that is confusing in itself, or one that asks for the same details twice; see [checkout forms that ask for the same address twice](/learn/redundant-entry). The menu's own current-page link uses the same attribute with another value; see [navigation that marks the current page only by its look](/learn/current-page).

---

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