An ordered set of steps that collapse into editable summaries as they are completed, ending in one explicit commit.
SetupFlow renders an ordered list (<ol>) and each SetupFlowStep renders a list item (<li>). The ordered list is the semantics: the sequence is meaningful, so assistive technology reads the steps in order and announces how many there are. Either element can be changed with the as prop when a different one is more appropriate.
A step draws its card with a Panel and lays its header and body out in one grid. The header row spans that grid, and on a step that can be reopened the row itself is a native <button> — the marker, the title, the summary, and the Edit affordance are all inside one control. The affordance is a Link rendered as a <span>, so it carries the link's appearance without being a second control nested in the first. On every other step the same row is a plain <div> with no interactivity at all.
Only the open step renders a body. The steps around it are not hidden — their content is genuinely absent from the DOM, so their fields, buttons, and headings leave the tab order and the accessibility tree entirely rather than lingering as unreachable nodes. A step that is not open is just its label, its summary, and, once it is complete, the header row that reopens it.
The flow does not own the accessibility of what a step contains. Labels, descriptions, error messages, and grouping for the inputs inside a step's body are the responsibility of those form components and the feature composing them.
| Element | Attribute | Description |
|---|---|---|
| Step | aria-current="step" | Set on the step that is currently open, exposing where in the flow the customer is. |
| Step body | role="group" | Applied to the open step's body, giving it a role that can hold a name — an element with no role exposes none, so the aria-labelledby beside it would otherwise do nothing. The step's fields get a boundary announced with the step's own title. |
| Step body | aria-labelledby | Points at the step's label, naming the group with the step it belongs to. |
| Step header | aria-labelledby | Names the control from the row's own elements: the Edit affordance, the step's title, and the summary it is showing — "Edit Name the export Weekly orders · CSV", or "Review Name the export 2 of 4 fields are no longer available" while the step needs re-confirming. Applied only on a step that can be reopened, where the row is the control. |
| Step marker | aria-hidden="true" | Applied to the marker coin so the decorative number, check, or caution glyph is hidden from assistive technology. |
On a step that can be reopened the header row is a <button>, and a button's accessible name replaces every bit of text inside it. The marker, the title, the summary and the Edit affordance are all inside that button, so a name written out in an aria-label would silence the summary — the record of what was chosen, and on an invalidated step the sentence that says what an earlier edit broke.
So the name is composed from the row's own elements with aria-labelledby, in the order they are read: the affordance, the title, then the summary if one is shown. It cannot drift from what is on screen, it reads the same whether the step's label is a string or rich content, and both frameworks build it the same way — the name a React step exposes is the name the Ember one exposes.
A step's status is drawn with color — muted for a step that has not been reached, inverted for the open one, success for a finished one, caution for one that needs another look. Color is never the only signal: the open step is bolded and carries aria-current="step", a finished step has collapsed to a one-line summary and gained a header row that reopens it, and a step that needs another look changes that row's text from Edit to Review and replaces its summary with one that names what changed.
Write the caution summary so it carries the message on its own. The marker's caution glyph is hidden from assistive technology, so a summary that says only that something is wrong leaves nothing for a screen reader to announce and nothing for a colorblind reader to distinguish from a finished step. The summary is announced with the row that reopens the step, because it is part of that row's name — see "Naming the header row" above.
The caution summary is drawn in text-caution-bold rather than the plain caution color, which is only 3.08:1 against a light surface and would fail WCAG AA at that size.
A step marked isDisabled renders no control of any kind: the flow steps over it when advancing, the same walk that picks the step an unseeded flow opens at, and it is never offered a way to be reopened. It therefore carries no aria-disabled — there is no widget for that state to sit on, and listitem does not support the attribute. What a disabled step exposes is what it is: a label and a summary, with nothing to operate. The state is published as a data-disabled attribute instead, which is what the flow reads when it works out which step comes next.
Only the marker recedes into the disabled palette. text-base-disabled is 2.29:1 against a light surface, so a title or summary drawn in it would fail WCAG AA and axe rejects it. The contrast requirement exempts text that is part of an inactive user interface component, which is what makes the palette usable on a disabled control — a step is not a control, so nothing here is exempt. The marker can use it because it is decorative and hidden from the accessibility tree; the words beside it stay at full strength.
Because the state is conveyed by the marker alone for a sighted reader, a step that is unavailable for a reason worth knowing should say so in its summary rather than relying on being drawn in grey.
Advancing the flow moves focus to the newly opened step's label, which carries tabindex="-1" so it can receive focus without joining the tab order. Without that move, focus would be left on a Continue button that no longer exists, and a keyboard or screen reader user would be returned to the top of the page with no indication that anything had happened. Landing on the label announces the step that now has to be filled in, and the fields inside it are the next stops on Tab.
The move only happens when the flow leaves one step for another. A flow that has just rendered does not take focus, so arriving on a screen that contains one leaves focus where the page put it. That holds for a flow given no defaultCurrentStep too: the step it finds a tick after mount opens without taking focus, the same as one named up front.
Markers are decorative and hidden from the accessibility tree. The number repeats the position the ordered list already conveys, and the check and caution glyphs restate a status that the summary, the body, and the header row's own text already carry — so nothing is lost when the marker is not perceived. This follows the same convention as Stepper.
The corollary is that a marker can never be the only place a status appears. See Markers for what each one shows.
The flow adds no key handling of its own. Every control in it is a native button, reachable and operable in the usual way, and a visible focus ring is shown on keyboard focus. A completed step is one tab stop, not two: the row is the button and the Edit affordance inside it is inert.
| Key | Function |
|---|---|
| Enter/Space | Activates the focused control: the action that moves the flow on, or the header row that reopens a completed step. |
| Tab | Moves focus to the next focusable element. Only the open step's contents are in the tab order. |
| Shift+Tab | Moves focus to the previous focusable element. |