Stepper
The Stepper component provides a vertical progress indicator for multi-step workflows like wizards and onboarding flows. It supports dynamic nested steps, multiple visual states, transition animations, navigation guards, and keyboard navigation.
It is composed of Stepper.Root, Stepper.StepGroup, and Stepper.Step to offer a structured and customizable interface.
Variations
Step states
Steps display different visual states based on their position relative to the active step and explicit state overrides.
Loading and blocking
Steps can show a loading spinner and optionally block forward navigation while an async operation is in progress.
Nested steps
A Stepper.StepGroup can contain dynamic nested steps that appear when the group is active. Nested steps support conditional rendering based on application state.
Indeterminate nested steps
When a step group has hasNestedSteps but no children are provided, a placeholder indicator with ”…” is shown, signaling that nested steps will be determined dynamically (e.g., after an API call resolves).
Here’s an example combining real nested steps (.1, .2, .3) on the active step group with an indeterminate placeholder on a following step group:
Navigation guard
Use the onBeforeChange prop to confirm navigation (e.g., warn users about losing progress when going back).
Completed state
When all steps are done, pass completed to show a “Complete” badge instead of the step counter. Completed steps still fade to 60% opacity by default so an in-progress step stays visually focused. For a finished flow that should stay fully opaque (no hover flash), pass disableCompletedFade.
Without connectors
Set showConnectors={false} to hide the vertical connector lines between steps.
Skeleton loading
When no children are passed, the stepper renders a skeleton placeholder. Control the number of skeleton items with skeletonCount.
Installation
import { Stepper } from "@harnessio/ui/components";
//...
const [currentStep, setCurrentStep] = useState("step-1");
return ( <Stepper.Root value={currentStep} onValueChange={setCurrentStep} title="Setup"> <Stepper.Step value="step-1" title="First Step" description="Do something" /> <Stepper.Step value="step-2" title="Second Step" description="Do another thing" /> <Stepper.Step value="step-3" title="Final Step" description="All done" /> </Stepper.Root>)Anatomy
All parts of the Stepper component can be imported and composed as required. A Stepper.Step rendered directly under Stepper.Root is a top-level step; a Stepper.Step rendered inside a Stepper.StepGroup is a nested step that appears only while the group is active.
<Stepper.Root> <Stepper.StepGroup> <Stepper.Step /> <Stepper.Step /> </Stepper.StepGroup> <Stepper.Step /></Stepper.Root>Stepper.Rootmanages the controlled step state, renders the header (title + progress counter), and provides context to the steps.Stepper.StepGroupis a step that can hold nested steps. Its nestedStepper.Stepchildren render only when the group is active;hasNestedStepsreserves their space ahead of time.Stepper.Stepis an individual step. States (completed/active/upcoming) are derived automatically from position; passstateto override witherrororskipped.
Best practices
- Unique values. Every step and nested step must have a unique
valueacross the entire stepper. - Controlled state. The component is always controlled — provide both
valueandonValueChange. - Blocking steps. Use
blockingfor async operations that must complete before proceeding (e.g., connectivity checks). - Navigation guards. Use
onBeforeChangeto warn about data loss, not to enforce linear progression. - Nested step cleanup. When nested steps unmount (e.g., the provider changes), the stepper automatically falls back to the parent step-group value.
- Skeleton state. Pass no children to show a skeleton — useful while step configuration loads asynchronously.
Accessibility
Keyboard
- The step list supports roving-tabindex navigation:
ArrowDown/ArrowUpmove to the next/previous focusable step,Homejumps to the first, andEndto the last. Focus clamps at the ends (no wrap-around). - Only the active step is in the tab order (
tabIndex=0); the others are reachable via the arrow keys. Collapsible step-content triggers activate withEnter/Space.
Labeling
- The active step is marked
aria-current="step". - Each step button has an
aria-labelwith position context (e.g. “Step 2 of 4: Configure”). - Step changes are announced through a visually-hidden
aria-live="polite"region.
Focus & states
- Disabled steps expose
aria-disabled="true"and are skipped by keyboard navigation. - Connectors, indicators, and skeleton placeholders are
aria-hiddendecorative elements. - Transition animations respect
prefers-reduced-motion(handled in CSS) and are disabled when the user requests reduced motion.
Properties
Root
The root container that manages stepper state, provides context to children, and renders the header with title and progress counter.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| onValueChange | true | (value: string) => void | |
| title | false | ReactNode | |
| completed | false | false | boolean |
| disableCompletedFade | false | false | boolean |
| showConnectors | false | true | boolean |
| collapsibleNestedSteps | false | false | boolean |
| onBeforeChange | false | (from: string, to: string) => boolean | string | |
| skeletonCount | false | 3 | number |
| className | false | string | |
| children | false | ReactNode |
StepGroup
A step that groups nested steps. Renders the indicator, title, description, connector, and optional nested Stepper.Step children.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| title | true | string | ReactNode | |
| description | false | string | ReactNode | |
| state | false | 'completed' | 'active' | 'upcoming' | 'skipped' | 'error' | |
| loading | false | false | boolean |
| blocking | false | false | boolean |
| hasNestedSteps | false | false | boolean |
| disabled | false | false | boolean |
| showStepBadge | false | false | boolean |
| totalStepsOverride | false | number | |
| stepNumberOverride | false | number | |
| hideStepNumber | false | boolean | |
| className | false | string | |
| children | false | ReactNode |
Step
An individual step. Rendered directly under Stepper.Root, it behaves as a top-level step; rendered inside a Stepper.StepGroup, it behaves as a nested step and is only rendered when the parent step group is active.
Prop | Required | Default | Type |
|---|---|---|---|
| value | true | string | |
| title | true | string | ReactNode | |
| description | false | string | ReactNode | |
| state | false | 'completed' | 'active' | 'upcoming' | 'skipped' | 'error' | |
| disabled | false | false | boolean |
| visualCompleted | false | false | boolean |
| contentOnly | false | false | boolean |
| headerActions | false | ReactNode | |
| showStepBadge | false | false | boolean |
| totalStepsOverride | false | number | |
| className | false | string | |
| children | false | ReactNode |