---
title: Stepper
description: Named step actions and progress indicators for multi-step processes.
icon: git-commit-horizontal
author:
  name: damianricobelli
  url: https://github.com/damianricobelli
search:
  keywords: [wizard, steps, multi-step]
mode: wide
---

<PoweredBy packages={[{ name: 'Stepperize', url: 'https://www.stepperize.com/' }]} />

### Installation

```package-install
npx shadcn@latest add @redpanda/stepper
```

<ComponentPreview name="stepper-demo" />

## When to use

Steppers guide users through multi-step processes. Use this decision tree:

```mermaid
graph TD;
    A[❓ Multi-step process?] --> B{Sequential steps?};
    A --> C[⚠️ Use single form/page];
    B --> D[✅ Use Stepper Component];
    B --> E{Non-linear flow?};
    D --> F{Step type?};
    E --> G[⚠️ Use Tabs or Navigation];
    F --> H[✅ Form Stepper];
    F --> I[✅ Progress Stepper];
    F --> J[✅ Wizard Stepper];

    classDef recommended fill:#4ade80,stroke:#22c55e,stroke-width:2px,color:#000;
    classDef discouraged fill:#fbbf24,stroke:#f59e0b,stroke-width:2px,color:#000;
    classDef decision fill:#60a5fa,stroke:#3b82f6,stroke-width:2px,color:#fff;
    classDef neutral fill:#9ca3af,stroke:#6b7280,stroke-width:2px,color:#fff;

    class D,H,I,J recommended;
    class C,G discouraged;
    class A,B,E,F decision;
```

## Usage

```tsx
const { Stepper } = defineStepper(
  {
    id: 'step-1',
    title: 'Step 1',
  },
  {
    id: 'step-2',
    title: 'Step 2',
  },
  {
    id: 'step-3',
    title: 'Step 3',
  },
);
```

```tsx
function CheckoutStepper() {
  const [navigationError, setNavigationError] = useState<string>();
  const reportNavigationError = (reason: unknown) => {
    setNavigationError(reason instanceof Error ? reason.message : 'Could not change steps.');
  };

  return (
    <>
      {navigationError && <p role="alert">{navigationError}</p>}
      <Stepper.Provider className="space-y-4" variant="horizontal">
        {({ methods }) => (
          <>
            <Stepper.Navigation aria-label="Checkout steps">
              {methods.steps.map((step) => (
                <Stepper.Step
                  key={step.id}
                  of={step.id}
                  title={<Stepper.Title>{step.title}</Stepper.Title>}
                  onClick={() => {
                    methods.goTo(step.id).catch(reportNavigationError);
                  }}
                />
              ))}
            </Stepper.Navigation>
            <Stepper.Panel>{methods.current.title} content</Stepper.Panel>
            <Stepper.Controls>
              <Button
                disabled={methods.isFirst}
                onClick={() => {
                  methods.prev().catch(reportNavigationError);
                }}
                variant="default"
              >
                Previous
              </Button>
              <Button
                onClick={() => {
                  (methods.isLast ? methods.reset() : methods.next()).catch(reportNavigationError);
                }}
              >
                {methods.isLast ? 'Reset' : 'Next'}
              </Button>
            </Stepper.Controls>
          </>
        )}
      </Stepper.Provider>
    </>
  );
}
```

## Anatomy and slots

`Stepper.Navigation` is a named navigation landmark containing an ordered list. Each `Stepper.Step` owns one list item and, in horizontal or vertical layouts, a native button. The button is named by its visible title; its numeral or icon is decorative.

- `title` is required. Pass text or a `Stepper.Title` element, including its polymorphic `render` prop.
- `description` contains supplementary content, such as `Stepper.Description` or a status `Badge`.
- `panel` contains inline step content. All supplied content renders directly, including fragments and multiple panels. Keep active-step conditions explicit. Horizontal flows may instead render one external `Stepper.Panel` from the current step.
- `Stepper.Step` does not accept marker children. Move them to these named props; there is no implicit child discovery.
- `Stepper.Title`, `Description`, `Panel` and `Controls` retain their Base UI `render` and native ref contracts.

```tsx
<Stepper.Step
  of={step.id}
  title={<Stepper.Title render={<span />}>{step.title}</Stepper.Title>}
  description={<Stepper.Description>Configure this step</Stepper.Description>}
  panel={methods.is(step.id) ? <Stepper.Panel>Step content</Stepper.Panel> : null}
  onClick={() => {
    methods.goTo(step.id).catch(reportNavigationError);
  }}
/>
```

## Keyboard and navigation

This is process navigation, not a tab widget. The current action exposes `aria-current="step"`; panels are ordinary content rather than artificial tabpanels. Tab and Shift+Tab visit enabled actions. Enter and Space invoke the native action. Arrow keys follow the layout: Left/Right horizontally, Up/Down vertically. Home and End focus the first and last eligible action. Focus movement stays inside its Provider and **does not activate a step**. Caller `preventDefault()` cancels the convenience key handling.

Use the action's `onClick` to validate before moving, or supply Stepperize's `beforeStepChange` guard. Rejected transitions must produce visible application feedback. `linear` makes policy-ineligible actions native disabled; a focused action never bypasses validation. Circle layout is a named, noninteractive progress indicator, with navigation provided by the flow's controls.

`Stepper.Step.variant` selects a finite Button tone recipe; `unstyled` and `null` are not supported. The current-step halo and keyboard-focus ring remain separate owned states.

### Sync the step with the URL

Pass the current step to `Stepper.Provider` and update the URL in `onStepChange`. The stepper then follows the URL, including the browser's back and forward buttons, with no extra component:

```tsx
const { step } = Route.useSearch();
const navigate = Route.useNavigate();

<Stepper.Provider step={step} onStepChange={(next) => navigate({ search: (prev) => ({ ...prev, step: next }) })}>
  {/* ... */}
</Stepper.Provider>;
```

:::info
`useStepper()` from `defineStepper` returns the Provider's stepper when it runs inside `Stepper.Provider`, so an existing child that calls `useStepper().goTo(step)` still drives the wizard. Outside a Provider it creates its own stepper.
:::

## Examples

### With description

<ComponentPreview name="stepper-with-description-demo" description="A stepper showing each step with a description." />

### With icon

<ComponentPreview name="stepper-with-icon-demo" description="A stepper showing each step with an icon." />

### With label orientation

<ComponentPreview
  name="stepper-with-label-orientation-demo"
  description="A stepper showing either horizontal or vertical label orientation."
/>

### Responsive

<ComponentPreview name="stepper-responsive-demo" description="A responsive stepper." />

### With tracking

<ComponentPreview
  name="stepper-with-tracking-demo"
  description="A stepper that scrolls the active panel into view when tracking is enabled."
/>

### With variants

<ComponentPreview
  name="stepper-with-variants-demo"
  description="A stepper displaying horizontal, vertical or circle variant."
/>

### With progress bar

<ComponentPreview
  name="stepper-with-progress-bar-demo"
  description="Stepper combining step titles with a linear progress bar."
/>

### With per-step status

<ComponentPreview
  name="stepper-with-status-pending-demo"
  description="Stepper showing current, previous and pending navigation states. These chips are not live provisioning results."
/>

## Props

<div className="flex flex-row gap-x-3">
  <ExternalLink href="https://www.stepperize.com/docs/v8/getting-started/installation" text="Docs" />
  <ExternalLink href="https://www.stepperize.com/docs/v8/api/react/types" text="API Reference" />
</div>

## Credits

- We take our inspiration from [Stepperize](https://www.stepperize.com/docs/v8/getting-started/installation) for the stepper component and style.
