# Progress Stepper
Shows a user's progress through a multi-step form, and what's coming next.
Section: components
Docs: https://nebula.kaluza.com/components/progress-stepper
---
## 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](/components/text-link) or [CTA Button](/components/cta-button) for explicit back navigation
## Anatomy
1. **Step counter**: the current step number and total step count, for example "1 of 4"
2. **Current step name**: the name of the step the user is on
3. **Next step label**: a preview of what is coming next, for example "Next: Delivery address"
4. **Progress bar**: a segmented bar, one segment per step, filled up to the current step
## Basic usage
```tsx
```
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.
```tsx
```
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.
```tsx
```
## 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.
## Rules and guidance
### Usage
- **ProgressStepper** SHOULD be used for flows with 3 to 8 steps. Rationale: Fewer than 3 steps do not need a persistent progress indicator. More than 8 steps indicates the flow should be restructured into stages. [id: progressstepper.step-range; testability: context]
### Interaction
- **ProgressStepper** WILL provide the step counter and step name as the heading's accessible name via aria-label. Rationale: The visual markup uses aria-hidden to keep the accessible name clean and unambiguous. [id: progressstepper.accessible-name; testability: auto]
- **ProgressStepper** WILL hide the internal ProgressBar from screen readers using aria-hidden. Rationale: The heading already communicates position; the visual bar is redundant for AT users. [id: progressstepper.progress-bar-hidden; testability: auto]
- **ProgressStepper** MUST NOT be used as a navigation control for returning to a previous step. Rationale: ProgressStepper is display-only. Use a Text Link or CTA Button for explicit back navigation. [id: progressstepper.no-navigation; testability: manual]
### Content
- **ProgressStepper** SHOULD use step names that are noun phrases, not instructions. Rationale: Noun phrases ("Delivery address") are shorter and more scannable than instructions ("Enter your delivery address"). [id: progressstepper.step-names; testability: manual]
- **ProgressStepper** SHOULD keep step names short enough not to wrap within the bar. [id: progressstepper.step-names-short; testability: manual]
- **ProgressStepper** SHOULD set finalStepLabel to match what actually happens next on the final step. Rationale: "Finish" implies submitting is immediate. If a review step follows, use the review step's name. [id: progressstepper.final-step-label; testability: manual]
### Composition
- **ProgressStepper** SHOULD use headingAs to match the heading level to the page's heading hierarchy. Rationale: The 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. [id: progressstepper.heading-level; testability: manual]
## Accessibility
### 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](https://www.w3.org/WAI/WCAG22/Understanding/headings-and-labels.html)
### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/info-and-relationships.html)
#### 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](https://www.w3.org/WAI/WCAG22/Understanding/status-messages.html)
### 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 each test covers:
- 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.
What these statuses mean:
- 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.
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.
## Properties
### ProgressStepper
Also accepts all props from `HTMLAttributes`.
- `currentStep` (type `number`, required). 1-indexed step the user is currently on.
- `steps` (type `string[]`, required). Step names, in order. The total step count is derived from this array's length.
- `finalStepLabel` (type `string`, optional, default `'Finish'`). Shown as "Next: " followed by this label once `currentStep` is the last step.
- `headingAs` (type `ComponentProps['as']`, optional, default `'h2'`). 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` (type `ElementType`, optional). Renders the component as a different element or component, e.g. `as="section"`.
- `forwardedAs` (type `ElementType`, optional). 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
Reference only; you do not need this to use the component. This is the CSS its styled elements use, with theme tokens shown by name. `${...}` is an interpolation (a theme token, a mixin or another value from the code). A `&[prop]` or `@when ` block holds CSS that applies only under that condition; these are notation, not valid CSS.
### progress-stepper-wrapper
```css
display: flex;
flex-direction: column;
gap: ${space[1]};
width: 100%;
```
### meta-row
```css
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
```css
font-weight: ${fontWeights.bold};
```
### step-counter
```css
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]};
}
```