---
id: "packages/active-ui/stepper"
title: "Stepper"
description: "Progress through a known sequence of steps, as a list with the current step marked; your application moves between steps."
url: "/packages/active-ui/stepper"
locale: "en"
revision: "ae7098cb96d7f7d5"
---

# Stepper

A stepper shows where the reader is in a flow of a few steps: an import, an application, a set-up.
It only shows: the buttons that move on and back sit below the content, in
[FormActions](/packages/active-ui/form-section). For the steps of a task that runs by itself, use
[Trace](/packages/active-ui/trace); for numbered instructions, use [Steps](/packages/active-ui/steps).

**Example: Progress through an import**

Client: Runs entirely in your browser. Source: `examples/ui/stepper.tsx`.

```tsx
// apps/docs/examples/ui/stepper.tsx
'use client';

import {useState} from 'react';

import {FormActions} from '@workspace/active-ui/components/composed/form-actions';
import {Stepper} from '@workspace/active-ui/components/composed/stepper';
import {Button} from '@workspace/active-ui/components/primitives/button';

const STEPS = [
  {id: 'file', label: 'Choose the file'},
  {id: 'columns', label: 'Match the columns'},
  {id: 'check', label: 'Check the rows'},
  {id: 'import', label: 'Import'},
];

/**
 * Progress through an import of four steps. The stepper only shows where
 * you are; the buttons below it move on and back. After the last step every
 * step counts as completed.
 */
export default function StepperExample() {
  const [step, setStep] = useState(1);
  return (
    <div className="grid w-full max-w-2xl gap-6">
      <Stepper
        steps={STEPS}
        currentStep={step}
        completedLabel="completed"
        aria-label="Import progress"
      />
      <p className="text-muted-foreground text-sm" aria-live="polite">
        {step < STEPS.length
          ? `Step ${step + 1} of ${STEPS.length}: ${STEPS[step]!.label}`
          : 'The import is done.'}
      </p>
      <FormActions>
        <Button
          variant="secondary"
          disabled={step === 0}
          onClick={() => setStep((current) => current - 1)}
        >
          Previous
        </Button>
        <Button
          disabled={step === STEPS.length}
          onClick={() => setStep((current) => current + 1)}
        >
          {step === STEPS.length - 1 ? 'Finish' : 'Next'}
        </Button>
      </FormActions>
    </div>
  );
}
```

## What the composition adds

- **State from one number.** Steps before `currentStep` are completed, the step at it is current,
  later steps are upcoming. `currentStep` equal to the number of steps means everything is done.
- **A real list.** An `<ol>` with `aria-current="step"` on the current step; a completed step
  carries a check mark and a hidden word, the current step a filled marker and a heavier label, so
  no state rests on colour alone.
- **Narrow screens stack** the steps; from the `sm` width they sit in a row that wraps, joined by
  lines. There is no orientation prop.

Steps are not clickable: which steps the reader may go back to is your flow's rule.

## Usage

```tsx
import {Stepper} from '@workspace/active-ui/components/composed/stepper';

<Stepper
  steps={[
    {id: 'file', label: 'Choose the file'},
    {id: 'check', label: 'Check the rows'},
  ]}
  currentStep={step}
  completedLabel="completed"
  aria-label="Import progress"
/>;
```

## What your application provides

- The steps with a stable `id` and a translated `label`, the current step, and the navigation.
- The translated word for completed steps, read by screen readers after the label.
- A name for the list (`aria-label`), and an announcement of the new step where moving does not
  move focus, as the example does with a polite line of text.

## API

| Prop | Type | Required | Description |
| --- | --- | --- | --- |
| `completedLabel` | `string` | yes | The translated word for "completed", for screen readers only.  Required, for the same reason as the required label of `StatusBadge`: the check mark is a shape and not a word, so without this text the difference between a completed and an upcoming step would only be visible. This component does not translate, so the text comes from the caller. |
| `currentStep` | `number` | yes | Zero-based index of the current step.  Everything before it counts as completed, everything after it as upcoming. A value equal to `steps.length` means everything is completed and there is no current step any more; that is a valid end state, not an error. |
| `steps` | `readonly StepperStep[]` | yes | The steps in order. At least one. |

**StepperStep**

```ts
interface StepperStep {
  id: string;
  label: string;
}
```

One step in a {@link Stepper}.

`id` is the identity and `label` the translated text. That separation is the
same as in `SearchCombobox` (`{value, label}`) and for the same reason:
identity must never run on a translated label, because then a step changes
identity as soon as the user picks another language. The key is also what a
caller with conditional steps needs: a sequence that changes length
reconciles wrongly on an index.

**StepperProps**

```ts
interface StepperProps extends Omit<
  React.ComponentProps<'ol'>,
  'children'
> {
  steps: readonly StepperStep[];
  currentStep: number;
  completedLabel: string;
}
```
