Skip to content

Stepper

beta

The Stepper component provides a vertical progress indicator for multi-step workflows like wizards and onboarding flows. It supports dynamic substeps, multiple visual states, transition animations, navigation guards, and keyboard navigation.

It is composed of Stepper.Root, Stepper.Step, and Stepper.SubStep to offer a structured and customizable interface.

Usage

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.

<Stepper.Root>
<Stepper.Step>
<Stepper.SubStep />
<Stepper.SubStep />
</Stepper.Step>
<Stepper.Step />
</Stepper.Root>

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.

SubSteps

Steps can contain dynamic substeps that appear when the parent step is active. SubSteps support conditional rendering based on application state.

Indeterminate SubSteps

When a step has hasSubSteps but no children are provided, a placeholder indicator with ”…” is shown, signaling that substeps will be determined dynamically (e.g., after an API call resolves).

Here’s an example combining real substeps (.1, .2, .3) on the active step with an indeterminate placeholder on a following step:

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.

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.

API Reference

Root

The root container that manages stepper state, provides context to children, and renders the header with title and progress counter.

<Stepper.Root
value={currentStep} // active step or substep value
onValueChange={setStep} // callback when step changes
title="Setup" // [OPTIONAL] displayed in the header
completed={false} // [OPTIONAL] shows "Complete" instead of counter
showConnectors={true} // [OPTIONAL] show connector lines between steps
onBeforeChange={guardFn} // [OPTIONAL] navigation guard
skeletonCount={3} // [OPTIONAL] skeleton items when no children
className="my-class" // [OPTIONAL] custom class on the nav element
>
{/* Step components */}
</Stepper.Root>
Prop
Required
Default
Type
valuetruestring
onValueChangetrue(value: string) => void
titlefalsestring
completedfalsefalseboolean
showConnectorsfalsetrueboolean
onBeforeChangefalse(from: string, to: string) => boolean | string
skeletonCountfalse3number
classNamefalsestring
childrenfalseReactNode

Step

An individual step in the stepper. Renders the indicator, title, description, connector, and optional substeps.

<Stepper.Step
value="connect" // unique identifier for this step
title="Connect" // step title (string or ReactNode)
description="Choose provider" // [OPTIONAL] description text
state="error" // [OPTIONAL] explicit state override
loading={false} // [OPTIONAL] show spinner on indicator
blocking={false} // [OPTIONAL] block forward navigation
disabled={false} // [OPTIONAL] prevent interaction
hasSubSteps={false} // [OPTIONAL] reserve substep space
className="my-class" // [OPTIONAL] custom class
>
{/* Optional Stepper.SubStep children */}
</Stepper.Step>
Prop
Required
Default
Type
valuetruestring
titletruestring | ReactNode
descriptionfalsestring
statefalse'error' | 'skipped'
loadingfalsefalseboolean
blockingfalsefalseboolean
disabledfalsefalseboolean
hasSubStepsfalsefalseboolean
classNamefalsestring
childrenfalseReactNode

SubStep

A substep nested within a parent step. Only rendered when the parent step is active.

<Stepper.SubStep
value="auth" // unique identifier for this substep
title="Authenticate" // substep title
description="OAuth with GitHub" // [OPTIONAL] description text
state="error" // [OPTIONAL] explicit state override
disabled={false} // [OPTIONAL] prevent interaction
className="my-class" // [OPTIONAL] custom class
/>
Prop
Required
Default
Type
valuetruestring
titletruestring | ReactNode
descriptionfalsestring
statefalse'error' | 'skipped'
disabledfalsefalseboolean
classNamefalsestring

Accessibility

The stepper implements the following accessibility features:

  • ARIA: Uses aria-current="step" on the active step, aria-label on each step button with position context, and aria-disabled for disabled steps
  • Live Region: Announces step changes via aria-live="polite" for screen readers
  • Keyboard Navigation: Supports ArrowDown, ArrowUp, Home, and End keys with roving tabindex
  • Reduced Motion: Respects prefers-reduced-motion by disabling transition animations

Best Practices

  1. Unique Values: Every step and substep must have a unique value across the entire stepper
  2. Controlled State: The component is always controlled — provide both value and onValueChange
  3. Blocking Steps: Use blocking for async operations that must complete before proceeding (e.g., connectivity checks)
  4. Navigation Guards: Use onBeforeChange to warn about data loss, not to enforce linear progression
  5. SubStep Cleanup: When substeps unmount (e.g., provider changes), the stepper automatically falls back to the parent step value
  6. Skeleton State: Pass no children to show a skeleton — useful while step configuration loads asynchronously