# 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]}; } ```