linear_scale

Stepper

Step-based progress and navigation flow.

Stepper Examples

Copy-ready examples with live previews.

Basic (static)

Use `value` to set the active step. Without `onChange`, it behaves as static.

Live preview

Basic static

import Stepper from "@/packages/ui/src/_components/stepper/Stepper";

export default function Example() {
  return (
    <Stepper
      aria-label="Checkout steps"
      value={1}
      steps={[
        { id: "a", label: "Cart" },
        { id: "b", label: "Delivery" },
        { id: "c", label: "Payment" },
      ]}
    />
  );
}

Clickable (controlled)

Provide `onChange` to enable clickable behavior and update your state.

Live preview

Clickable (controlled)

"use client";

import { useState } from "react";
import Stepper from "@/packages/ui/src/_components/stepper/Stepper";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <Stepper
      value={active}
      onChange={(nextIndex) => setActive(nextIndex)}
      steps={[
        { id: "a", label: "Account" },
        { id: "b", label: "Profile" },
        { id: "c", label: "Done" },
      ]}
    />
  );
}

Badges + support text + content panels

Each step can render a badge, a secondary line, and an optional content panel.

Live preview

Content panels (active)

"use client";

import { useState } from "react";
import Stepper from "@/packages/ui/src/_components/stepper/Stepper";
import Icon from "@/packages/ui/src/_components/icon/Icon";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <Stepper
      value={active}
      onChange={(next) => setActive(next)}
      contentMode="active"
      steps={[
        {
          id: "a",
          badge: <Icon name="person" style="rounded" size="medium" variant="primary" fill={1} />,
          label: "Account",
          supportText: "Basic details",
          content: <div>Content: Account</div>,
        },
        {
          id: "b",
          badge: <Icon name="home" style="rounded" size="medium" variant="secondary" fill={1} />,
          label: "Address",
          supportText: "Shipping & billing",
          content: <div>Content: Address</div>,
        },
      ]}
    />
  );
}

Orientation

Horizontal for wizards, vertical for side layouts and long labels.

Live preview

Horizontal + vertical

"use client";

import { useState } from "react";
import Stepper from "@/_components/stepper/Stepper";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <>
      <Stepper
        value={active}
        onChange={(next) => setActive(next)}
        orientation="horizontal"
        aria-label="Horizontal stepper"
        steps={[
          { id: "a", label: "Account" },
          { id: "b", label: "Address" },
          { id: "c", label: "Payment" },
        ]}
      />

      <Stepper
        value={active}
        onChange={(next) => setActive(next)}
        orientation="vertical"
        aria-label="Vertical stepper"
        steps={[
          { id: "a", label: "Account" },
          { id: "b", label: "Address" },
          { id: "c", label: "Payment" },
        ]}
      />
    </>
  );
}

Size

Use `sm` for dense UI, `md` for default, and `lg` for emphasis.

Live preview

sm / md / lg

"use client";

import { useState } from "react";
import Stepper from "@/_components/stepper/Stepper";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <>
      <Stepper value={active} onChange={setActive} size="sm" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={active} onChange={setActive} size="md" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={active} onChange={setActive} size="lg" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
    </>
  );
}

Variant

Variants are semantic; your CSS defines the tones.

Live preview

All variants

import Stepper from "@/_components/stepper/Stepper";

export default function Example() {
  return (
    <>
      <Stepper value={0} variant="primary" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="secondary" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="tertiary" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="success" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="warning" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="danger" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
      <Stepper value={0} variant="info" steps={[{ id: "a", label: "A" }, { id: "b", label: "B" }]} />
    </>
  );
}

Flow

`default` tries to fit; `scroll` enables horizontal scrolling for many steps.

For docs/playgrounds: wrap horizontal steppers with `overflowX: auto` so they never break layout.

Live preview

flow="default"
flow="scroll"

Default vs scroll

"use client";

import { useMemo, useState } from "react";
import Stepper from "@/_components/stepper/Stepper";

export default function Example() {
  const stepsMany = useMemo(
    () => Array.from({ length: 12 }).map((_, i) => ({ id: String(i), label: "Step " + (i + 1) })),
    []
  );

  const stepsFew = useMemo(() => stepsMany.slice(0, 6), [stepsMany]);

  const [a, setA] = useState(2);
  const [b, setB] = useState(3);

  return (
    <>
      <Stepper value={a} onChange={setA} steps={stepsFew} flow="default" />
      <Stepper value={b} onChange={setB} steps={stepsMany} flow="scroll" />
    </>
  );
}

Content mode

Panels are always rendered; `contentMode` controls their visibility via CSS classes.

Live preview

none / active / all

import Stepper from "@/_components/stepper/Stepper";

export default function Example() {
  return (
    <>
      <Stepper value={0} contentMode="none" steps={[{ id: "a", label: "A", content: <div>Panel A</div> }]} />
      <Stepper value={0} contentMode="active" steps={[{ id: "a", label: "A", content: <div>Panel A</div> }]} />
      <Stepper value={0} contentMode="all" steps={[{ id: "a", label: "A", content: <div>Panel A</div> }]} />
    </>
  );
}

Status + linear mode

Status can be set explicitly per step. `linear` blocks jumping too far ahead.

Live preview

Tip: with linear, users can’t jump ahead more than one step.

Statuses + linear

"use client";

import { useState } from "react";
import Stepper from "@/packages/ui/src/_components/stepper/Stepper";

export default function Example() {
  const [active, setActive] = useState(0);

  return (
    <Stepper
      value={active}
      onChange={setActive}
      linear
      steps={[
        { id: "a", label: "Done", status: "complete" },
        { id: "b", label: "Error", status: "error" },
        { id: "c", label: "Disabled", status: "disabled" },
        { id: "d", label: "Default", status: "default" },
      ]}
    />
  );
}

On this page

Stepper Props

Stepper — Props
PropTypeRequiredDefaultDescription
stepsStepperStep[]YesSteps to render (label/supportText/badge/content + optional status).
valuenumberYesActive step index (0-based).
onChange(nextIndex: number, meta?: StepperChangeMeta) => voidNoEnables clickable behavior (unless `interaction='static'`). Called when a step is clicked and allowed.
orientation"horizontal" | "vertical"No"horizontal"Layout orientation.
size"sm" | "md" | "lg"No"md"Size modifier.
variant"primary" | "secondary" | "tertiary" | "success" | "warning" | "danger" | "info"No"primary"Visual tone / semantic variant.
flow"default" | "scroll"No"default"Flow behavior. `scroll` adds the scroll modifier class for long steppers.
interaction"clickable" | "static"NoonChange ? "clickable" : "static"Forces whether steps can be clicked.
contentMode"none" | "active" | "all"No"none"Controls content visibility via CSS classes (panels are still rendered).
linearbooleanNofalseWhen true, users can’t jump ahead more than one step.
aria-labelstringNoPassed to the root `<nav aria-label=...>` for accessibility.
classNamestringNoAppended to the root classes.
customStylesReact.CSSPropertiesNoInline styles applied to the root `<nav>` element.