Search documentation

Search Stepper docs, API, guides, and release notes.

component

Stepper

Beta

A composable primitive for building accessible multi-step flow architecture with local source ownership.

Examples

Start with one production-shaped flow before reaching for adapters or recipes.

Checkout flow

Checkout flow

Primitive

A compact preview that starts at the cart and advances one step at a time.

Installation

Add the primitive through the shadcn registry. The command copies the source into your app.

pnpm dlx shadcn@latest add @stepper/stepper

Usage

Import the primitive parts you need, then compose the list, items, and controls directly in JSX.

imports.tsx
Copy code is available in the top right.
import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperLabel,
  StepperList,
  StepperNext,
  StepperPrevious,
  StepperTrigger,
  useStepperItem,
} from "@/components/ui/stepper";
checkout-flow.tsx
Copy code is available in the top right.
import * as React from "react";
import { Check, CreditCard, ShoppingCart, Truck } from "lucide-react";

import {
  Stepper,
  StepperContent,
  StepperDescription,
  StepperIndicator,
  StepperItem,
  StepperLabel,
  StepperList,
  StepperNext,
  StepperPrevious,
  StepperTrigger,
  useStepperItem,
} from "@/components/ui/stepper";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";

type StepValue = "cart" | "shipping" | "payment";

export function CheckoutFlow() {
  const [value, setValue] = React.useState<StepValue>("cart");
  const [showLabels, setShowLabels] = React.useState(true);

  return (
    <Stepper value={value} onValueChange={(next) => setValue(next as StepValue)}>
      <StepperList>
        <CheckoutStep
          value="cart"
          title="Cart"
          description="Review items"
          icon={ShoppingCart}
          showLabels={showLabels}
        />
        <CheckoutStep
          value="shipping"
          title="Shipping"
          description="Delivery details"
          icon={Truck}
          showLabels={showLabels}
        />
        <CheckoutStep
          value="payment"
          title="Payment"
          description="Secure checkout"
          icon={CreditCard}
          showLabels={showLabels}
        />
      </StepperList>

      <StepperContent value="cart">Review selected products.</StepperContent>
      <StepperContent value="shipping">Confirm delivery details.</StepperContent>
      <StepperContent value="payment">Choose a payment method.</StepperContent>

      <div className="mt-6 flex justify-between">
        <StepperPrevious />
        {value === "payment" ? (
          <Button type="button">Place order</Button>
        ) : (
          <StepperNext />
        )}
      </div>

      <label className="mt-4 flex items-center justify-center gap-2 border-t pt-4">
        <Checkbox
          checked={showLabels}
          onCheckedChange={(checked) => setShowLabels(checked === true)}
        />
        Show labels
      </label>
    </Stepper>
  );
}

function CheckoutStep({
  value,
  title,
  description,
  icon: Icon,
  showLabels,
}: {
  value: StepValue;
  title: string;
  description: string;
  icon: React.ComponentType<{ className?: string }>;
  showLabels: boolean;
}) {
  const { stepPosition, stepState } = useStepperItem<StepValue>();
  const isComplete = stepState === "completed" || stepPosition === "previous";

  return (
    <StepperItem value={value} defaultTrigger={false}>
      <StepperTrigger>
        <StepperIndicator>
          {isComplete ? <Check /> : <Icon />}
        </StepperIndicator>
        <span className="relative block min-h-10 min-w-0">
          <span
            className={
              showLabels
                ? "flex min-w-0 flex-col gap-1"
                : "invisible flex min-w-0 flex-col gap-1 opacity-0"
            }
          >
            <StepperLabel>{title}</StepperLabel>
            <StepperDescription>{description}</StepperDescription>
          </span>
        </span>
      </StepperTrigger>
    </StepperItem>
  );
}

Files

The registry installs a single editable primitive into your local component folder.

stepper.tsx

Composition

Use the following composition to build a Stepper.

stepper-composition.txt
Copy code is available in the top right.
Stepper
├── StepperList
│   ├── StepperItem
│   │   ├── StepperTrigger
│   │   │   ├── StepperIndicator
│   │   │   ├── StepperLabel
│   │   │   └── StepperDescription
│   │   └── StepperSeparator
│   └── StepperItem
├── StepperContent
├── StepperPrevious
└── StepperNext

Order model

Composed mode registers StepperItem primitives in mount order for simple JSX flows. Pass steps when a flow is dynamic, conditional, routed, or rendered from data so the order stays explicit.

Headless composition

Hide the visual list and drive custom controls with useStepper when the product surface needs a different layout.

headless-composition.txt
Copy code is available in the top right.
Stepper
├── StepperList className="sr-only"
│   └── StepperItem
├── CustomStepNavigation
├── StepperContent
└── CustomFooter using useStepper()

API Reference

The public surface stays small: root state, ordered list, item state, trigger composition, content panels, and navigation helpers.

PropTypeDefaultDescription
valuestring-Controlled active step value.
defaultValuestring-Initial active step for uncontrolled usage.
onValueChange(value: string) => void-Called when the selected step changes, including controlled fallbacks to the first enabled step.
orientation"horizontal" | "vertical""horizontal"Controls list layout and connector direction.
steps{ value: string; disabled?: boolean }[]-Optional explicit step list for headless, dynamic, or conditional flows. When provided, Stepper uses it instead of composed StepperItem registration order.
PropTypeDefaultDescription
aria-labelstring"Progress steps"Accessible name for the ordered list. Use aria-labelledby when the list has an external heading.
PropTypeDefaultDescription
valuestring-Unique step id used to connect trigger and content.
completedbooleanfalseMarks a step as completed and updates data-state.
defaultTriggerbooleantrueControls whether StepperItem renders the default trigger markup. Set it to false when composing your own StepperTrigger.
disabledbooleanfalseDisables the trigger and skips the step in next/previous navigation.
errorbooleanfalseMarks a step as needing attention and exposes data-state="error".
separatorbooleantrueControls the default connector separator after this item. Set to false when rendering a custom StepperSeparator.
asChildbooleanfalseRender the trigger props onto a custom button or link with Radix Slot.
disabledbooleanfalseDisables the trigger in addition to the parent StepperItem disabled state. With asChild, aria-disabled and tabIndex are applied instead of a native disabled attribute.
PropTypeDefaultDescription
valuestring-Step value this content panel belongs to.
forceMountbooleanfalseKeeps inactive content mounted and hidden. Prefer keepMounted for form persistence semantics.
keepMountedbooleanfalseSemantic alias for forceMount, useful when preserving form or adapter state between steps.
asChildbooleanfalseRender the content props onto a child element with Radix Slot.
PropTypeDefaultDescription
asChildbooleanfalseRender StepperPrevious or StepperNext onto a custom button primitive with Radix Slot.
disabledbooleanfalseDisables the navigation button in addition to canGoPrevious or canGoNext.
onBeforePrevious() => boolean | Promise<boolean>-Runs before StepperPrevious changes the value. Return false to cancel navigation.
onBeforeNext() => boolean | Promise<boolean>-Runs before StepperNext changes the value. Return false to cancel navigation.