Steps

Where the reader is in a sequence, and what is left. Horizontal, vertical, inline or as navigation, with a status per step.

Accessibility

Steps is a picture of a process, rendered as plain divs. Most of the work is saying in text what the circles and colors show only to the eye.

What it renders

Steps renders plain <div>s. There is no list role, no aria-current on the step in progress and no announcement like “3 of 5”. The kit changes none of that.
A step's state reaches a screen reader only if you put it in text. A tick in a colored circle and a cross in a red one are decoration to the accessibility tree.
  • Put the state in words where it matters: a subTitle of “Done”, “In progress” or “Failed” is read out, and it works for a reader who cannot tell the circle colors apart.
  • When the rail is the only thing that moved, announce the change separately: a live region beside the flow, or focus moved to the new step's heading.
  • When the rail sits above panels that swap, give each panel a heading, so the rail is a summary and not the only signal.

Naming a step and the rail

  • A step's name is its title text. A step with an icon and no title has no name, and nothing warns about it.
  • An item's icon replaces what is inside the circle, so an icon-only rail reads as nothing. Keep the title, or put a word in the icon slot with sr-only.
  • A string subTitle is also written to a title attribute. That is a hover tooltip, and it does not name the step.
  • In type="inline", an item's content shows in a tooltip on hover. Keyboard and touch users cannot reach it unless the row is clickable, so put nothing there they need.
  • The rail itself has no name. When a page has more than one, wrap each in a labeled region or put a heading above it.

Keyboard reach

A step is focusable only when it is clickable: Steps has an onChange or the item has an onClick, and the item is not disabled. The step then gets role="button", tabIndex=0 and an Enter or Space handler.
  • There is no roving tab stop and no arrow keys. Every clickable step is its own tab stop, so a ten-step rail takes ten presses to pass. Keep a clickable rail short, or use maxCount.
  • onChange does not fire for the current step, so that one is a focusable button that does nothing, and nothing marks it as unavailable.
  • A clickable step shows a focus outline on :focus-visible and a pointer cursor on hover. Keep both: they are the only signs that the rail is interactive.
  • A rail with no onChange is not in the tab order, which is right for a readout.
  • After a click moves the flow, move focus into the panel that changed. Focus left on the rail tells the reader nothing about where they went.

Disabled and unreachable steps

  • disabled on an item removes the role, the tab stop and the key handler. It is not aria-disabled: the step leaves the tab order, so a keyboard user cannot land on it to find out why.
  • So the step's own text is the only place for the reason. “Locked until payment clears” in subTitle or content explains what a grayed circle cannot.
  • The ellipsis step maxCount creates is disabled, so the hidden steps have no keyboard route and no name. When they matter, list them somewhere else too.
  • Do not let gray be the only difference between a waiting step and a disabled one. They mean different things.

Contrast of the rail and the marks

Every color comes from the active theme, so contrast depends on the theme. After changing the seed colors, check these four pairs by hand.
  • The connector between two circles is the quietest thing on the rail and it carries the sequence. As a non-text graphic it needs 3:1 against the surface behind it, including a tinted card.
  • The tick inside a finished circle sits on the circle's fill, not on the page. Check it against that fill.
  • A waiting step's title is low emphasis, but it is still text and still needs 4.5:1.
  • The error state is red and a cross. The cross is what carries it for a reader who cannot see the red, so an item icon that replaces the cross also removes the only signal that is not color.
The Tokens tab lists which tokens this component reads.

Motion

The built-in transitions are short: the rail and the circles fade between states. Turn them off with ThemeScope theme={{ token: { motion: false } }} for a reader who has asked for reduced motion.
Put anything you add on top (a step sliding in, a ring that spins during an upload) behind motion-safe:, and pair a ring that keeps animating with text that says what is happening.