On this page
Progress Stepper
Shows a user's progress through a multi-step form, and what's coming next.
- Overview
- Rules and guidance
- Properties
- Styles
- Accessibility
When to use
- For multi-step forms with 3 to 8 steps, to help users understand where they are and how much is left
- To show what's coming up next, alongside where the user currently is
When not to use
- For flows with more than 8 steps - restructure the flow into stages first
- To let users navigate back to a previous step by clicking - Progress Stepper is display-only. Use a Text Link or CTA Button for explicit back navigation
Anatomy

- Step counter: the current step number and total step count, for example "1 of 4"
- Current step name: the name of the step the user is on
- Next step label: a preview of what is coming next, for example "Next: Delivery address"
- Progress bar: a segmented bar, one segment per step, filled up to the current step
Basic usage
Edit live code
<ProgressStepper currentStep={2} steps={['Your details', 'Delivery address', 'Payment', 'Review']} />
The current step name and next-step label are derived from steps. currentStep is 1-indexed, matching the step counter shown to the user.
Final step
When the user is on the final step, use finalStepLabel to set what appears in place of the next-step label.
Edit live code
<ProgressStepper currentStep={4} steps={['Your details', 'Delivery address', 'Payment', 'Review']} finalStepLabel="Finish" />
Use "Finish" only when submitting is the very next action. If a review or confirmation step follows, use its name instead.
Heading level
Progress Stepper renders the current step name as an h2 by default. Use headingAs to match it to the heading hierarchy of the page it sits on.
Edit live code
<ProgressStepper currentStep={1} steps={['Your details', 'Delivery address', 'Payment']} headingAs="h3" />
Content
Step names
- Keep step names short: long names may wrap awkwardly
- Use a noun phrase describing the step's content ("Delivery address"), not an instruction ("Enter your delivery address")
Final step label
Match the label to what actually happens next. "Finish" is only right when submitting is the very next action.
usage
- ProgressStepper should be used for flows with 3 to 8 stepsFewer than 3 steps do not need a persistent progress indicator. More than 8 steps indicates the flow should be restructured into stages.Copy rule ID for progressstepper.step-range
interaction
- ProgressStepper will provide the step counter and step name as the heading's accessible name via aria-labelThe visual markup uses aria-hidden to keep the accessible name clean and unambiguous.Copy rule ID for progressstepper.accessible-name
- ProgressStepper will hide the internal ProgressBar from screen readers using aria-hiddenThe heading already communicates position; the visual bar is redundant for AT users.Copy rule ID for progressstepper.progress-bar-hidden
- ProgressStepper must not be used as a navigation control for returning to a previous stepProgressStepper is display-only. Use a Text Link or CTA Button for explicit back navigation.Copy rule ID for progressstepper.no-navigation
content
- ProgressStepper should use step names that are noun phrases, not instructionsNoun phrases ("Delivery address") are shorter and more scannable than instructions ("Enter your delivery address").Copy rule ID for progressstepper.step-names
- ProgressStepper should keep step names short enough not to wrap within the barCopy rule ID for progressstepper.step-names-short
- ProgressStepper should set finalStepLabel to match what actually happens next on the final step"Finish" implies submitting is immediate. If a review step follows, use the review step's name.Copy rule ID for progressstepper.final-step-label
composition
- ProgressStepper should use headingAs to match the heading level to the page's heading hierarchyThe default h2 is correct for a Page component that owns the h1. Override when the stepper sits in a context with a different heading structure.Copy rule ID for progressstepper.heading-level
ProgressStepper
Also accepts all props from HTMLAttributes.
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
currentStep | number | — | Yes | 1-indexed step the user is currently on. |
steps | string[] | — | Yes | Step names, in order. The total step count is derived from this array's length. |
finalStepLabel | string | 'Finish' | No | Shown as "Next: " followed by this label once `currentStep` is the last step. |
headingAs | ComponentProps<typeof Heading3>['as'] | 'h2' | No | Semantic element the current step name renders as, via Heading3's own `as` prop - use this to match the heading level to where the stepper sits in the page's heading hierarchy. Visual style always matches Heading3 regardless of the element used. Defaults to `h2`, since the standard Page component already owns the page's `h1` - override this if you're on an older Page component from before that convention existed. |
as | ElementType | — | No | Renders the component as a different element or component, e.g. `as="section"`. |
forwardedAs | ElementType | — | No | Like `as`, but passed on to the component being wrapped, for when a styled wrapper should keep its own element and change the one beneath it. |
Styles
progress-stepper-wrapper
display: flex;
flex-direction: column;
gap: ${space[1]};
width: 100%;
meta-row
margin: 0;
font-family: ${fonts.body};
font-weight: ${fontWeights.normal};
line-height: ${lineHeights.small};
color: ${colors.body};
font-size: ${responsiveFontSizes.small[0]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.small[1]};
}
next-step-name
font-weight: ${fontWeights.bold};
step-counter
display: block;
font-family: ${fonts.body};
font-weight: ${fontWeights.normal};
line-height: ${lineHeights.small};
color: ${colors.body};
font-size: ${responsiveFontSizes.small[0]};
margin-bottom: ${space[1]};
${mediaQueries.smallAndUp} {
font-size: ${responsiveFontSizes.small[1]};
}
In brief
Progress Stepper is a heading with a hidden bar and a "Next" line. A screen reader announces the heading as "Step 1 of 4, Your details", then the next step.
What Nebula provides
- The step name is a heading, h2 by default, named with the step count and the step name together.
- The segmented bar is hidden from screen readers, because the heading already says the same in words.
- Nothing carries role="progressbar" or aria-current, so the heading keeps its semantics.
Accessible design
Name each step with a short noun phrase that reads after "Step 2 of 4,"
The step name is announced straight after the count, so "Delivery address" works and "Enter your delivery address" does not.
WCAG: 2.4.6 Headings and Labels
Accessible development
Set headingAs so the step heading fits the page outline
The default h2 assumes the Page component owns the h1. If the page is built differently, pass the level that keeps the outline in order.
WCAG: 1.3.1 Info and Relationships
Don't add role="progressbar" or aria-current to the stepper
The progressbar role makes every descendant presentational, which would strip the heading of its meaning. There are no separately labelled steps for aria-current to point at.
WCAG: 1.3.1 Info and Relationships
If the step changes without a page load, announce the new step
The heading is not a live region, so a step that changes in place is silent. Announce the new step in a status message, or load a new page.
WCAG: 4.1.3 Status Messages
Accessibility testing status
Reviewed on 9 October 2026 against v17.0.1.
| Test | Status | Tested with |
|---|---|---|
| Default state | Tested | axe-core via the Storybook test runner (CI) |
| Advanced states | Tested | axe-core via the Storybook test runner (CI) |
| Keyboard navigation | Manually tested | keyboard (reported by the owner) |
| Screen reader | Partially tested | VoiceOver with Chrome (reported by the owner) |
What these tests and statuses mean
The table records which checks were run on this component on its own. It is not a statement of WCAG conformance, and it does not cover how the component is used.
The tests
- Default state: The component as it first appears, before anything is clicked or changed.
- Advanced states: Every other state and variant: error, open, selected, each size and style, and hovering.
- Keyboard navigation: Whether every function can be reached and used with the keyboard alone, with a visible focus ring.
- Screen reader: Whether a screen reader announces what is on screen correctly: the name, the role and any state.
The statuses
- Tested: Every check in this row passed in an automated run that repeats on every pull request.
- Manually tested: Every check in this row was run once, by a person or with a tool they started, and all of them passed. It is not re-run on every change.
- Partially tested: Some checks in this row were run once and passed; the rest are still to do.
- Issue found: A check in this row failed. The ticket is shown beside the status.
- Not tested: No check in this row has been run yet.
- Not applicable: This row does not apply to the component, for the reason given beside it.