Stepper

Shows how far a procedure has come: which steps are done, which one is being worked on, which lie ahead and which failed (also called a progress tracker). Reach for it for approvals, rollouts and closing routines.

import { Stepper } from "@umriss-ui/core";

Each state is said as a word as well as drawn, so a screen reader hears "done", "current step", "upcoming" or "failed". A failed step stays failed when the procedure moves past it; past the last step, every step is done.

Examples

Where a procedure stands

Pass the steps in order and the index of the current one; the steps before it read as done, the ones after as upcoming.

import { Stepper } from "@umriss-ui/core";

export default function WhereAProcedureStands() {
  return (
    <Stepper
      aria-label="Incident INC-1048"
      steps={[{ label: "Detect" }, { label: "Acknowledge" }, { label: "Mitigate" }, { label: "Resolve" }, { label: "Review" }]}
      current={2}
      style={{ maxWidth: 720 }}
    />
  );
}

Steps in a column

Set orientation to vertical where each step needs a description beneath its label, such as a deadline.

import { Stepper } from "@umriss-ui/core";

const CLOSE = [
  { label: "Post all invoices", description: "Until Tuesday, 3 March" },
  { label: "Reconcile accounts", description: "Bank, payables, receivables" },
  { label: "Book accruals", description: "Bonuses and open orders" },
  { label: "Report to the board", description: "Friday, 6 March" },
];

export default function StepsInAColumn() {
  return <Stepper aria-label="Closing February" orientation="vertical" steps={CLOSE} current={2} style={{ maxWidth: 360 }} />;
}

A failed step

Mark a step failed and it shows a cross and says "failed"; it stays failed when the procedure has moved past it.

import { Stepper } from "@umriss-ui/core";

export default function AFailedStep() {
  return (
    <Stepper
      aria-label="Shipment FP-1004249"
      orientation="vertical"
      steps={[
        { label: "Loaded", description: "North depot, 06:40" },
        { label: "Out for delivery", description: "Tour T-01" },
        { label: "First attempt", description: "10:35, nobody at the door", failed: true },
        { label: "Second attempt", description: "Tomorrow, 08:00–10:00" },
        { label: "Delivered" },
      ]}
      current={3}
      style={{ maxWidth: 360 }}
    />
  );
}

Move on yourself

The stepper has no keys of its own: your application sets current after its own check, here behind two buttons.

import { useState } from "react";
import { Button, Stack, Stepper } from "@umriss-ui/core";

const STEPS = [{ label: "Choose people" }, { label: "Set capacity" }, { label: "Plan leave" }, { label: "Confirm" }];

export default function MoveOnYourself() {
  const [current, setCurrent] = useState(1);

  return (
    <Stack gap={4} style={{ maxWidth: 560 }}>
      <Stepper aria-label="Set up sprint 15" steps={STEPS} current={current} />
      <Stack direction="row" gap={2}>
        <Button size="sm" variant="secondary" disabled={current === 0} onClick={() => setCurrent(current - 1)}>
          Back
        </Button>
        <Button size="sm" variant="primary" disabled={current === STEPS.length} onClick={() => setCurrent(current + 1)}>
          {current >= STEPS.length - 1 ? "Start the sprint" : "Next"}
        </Button>
      </Stack>
    </Stack>
  );
}

A deployment

At a card's head the stepper shows where a rollout stands; a failed step is marked in place and an alert offers the way on.

import { useState } from "react";
import { Alert, Button, Card, CardBody, CardHeader, ProgressBar, Stack, Stepper, Text } from "@umriss-ui/core";

const STEPS = [
  { label: "Build" },
  { label: "Test" },
  { label: "Canary", description: "5 % of traffic, 15 minutes" },
  { label: "Roll out", description: "All regions" },
  { label: "Verify" },
];

export default function ADeployment() {
  const [failed, setFailed] = useState(false);
  const current = 2;
  const steps = STEPS.map((step, index) => (index === current && failed ? { ...step, failed: true } : step));

  return (
    <Card style={{ maxWidth: 720 }}>
      <CardHeader
        eyebrow="Checkout · release 2026.03.17-2"
        title="Cap payment retries at one"
        actions={
          <Button size="sm" variant="ghost" onClick={() => setFailed(true)} disabled={failed}>
            Fail the canary
          </Button>
        }
      />
      <CardBody>
        <Stack gap={5}>
          <Stepper aria-label="Deployment" steps={steps} current={current} />
          {failed ? (
            <Alert
              tone="danger"
              title="Canary stopped"
              actions={
                <Button size="sm" variant="secondary" onClick={() => setFailed(false)}>
                  Retry the canary
                </Button>
              }
            >
              The error rate on the canary rose to 2.4 %, above the 1 % the rollout allows.
            </Alert>
          ) : (
            <Stack gap={2}>
              <Text size="sm">Canary on 5 % of traffic - 6 of 15 minutes</Text>
              <ProgressBar value={0.4} label="Canary" valueText="6 of 15 minutes" />
            </Stack>
          )}
        </Stack>
      </CardBody>
    </Card>
  );
}

When to use something else

Keyboard

The keys of Button apply here.

Accessibility

An ordered list, so a screen reader says how many steps there are and which one it reads. The current step carries aria-current="step"; every other step's state follows its label as a word only a screen reader hears, and the drawn number, tick or cross is hidden from it.

It announces nothing when current moves on: a reader hears the new state where it reads the list. A step that must be heard as it fails is said in an Alert; a share of a step's work stands in a ProgressBar, as in the deployment.

Under forced colours the lines between the steps take the system's text colour, and the current step's marker an outline in the selection colour.

API

StepperProps

NameTypeDefaultDescription
stepsrequiredreadonly StepperStep[]—The steps in the order they are walked.

Shown in: Where a procedure stands, Steps in a column, A failed step and 6 more

currentrequirednumber—The index of the step being worked on. The steps before it are done, the ones after it upcoming; past the last step, every step is done.

Shown in: Where a procedure stands, Steps in a column, A failed step and 6 more

orientation"horizontal" | "vertical""horizontal"horizontal in a row, vertical in a column for steps with longer descriptions.

Shown in: Steps in a column, A failed step, An invoice in tabs (Tabs)

refRef<HTMLOListElement>—Receives the component's root element. from RefProp

Shown in: A row that holds still (Sizes), An anchored surface (Popover), Take the width of the trigger (Popover) and 2 more

Also takes every attribute of the rendered element.

StepperStep

NameTypeDefaultDescription
labelrequiredReactNode—What the step is called - a verb for what happens in it.

Shown in: Where a procedure stands, A failed step, Import budgets (FileInput) and 1 more

descriptionReactNode—A line beneath the label - how long the step takes, what it needs.

Shown in: A failed step, An invoice in tabs (Tabs)

failedboolean—The step failed. It says so whether it is behind the current step or is the current one: a failed step is not done.

Shown in: A failed step, An invoice in tabs (Tabs)

Known limits

What umriss deliberately does not build, and why: ADR-0032.

Every page of @umriss-ui/core

Getting started

Customising

Layout

Typography

Actions

Forms

Choosing

Dates and times

Feedback

Overlays

Navigation

Data display

API index