# React Stepper Component: States, Variants and Phones

> A React stepper component guide for checkout, onboarding and KYC: circles, bars or dots, what each step state means, and how a stepper collapses on a phone.

- Author: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Published: Oct 13, 2026
- Category: [Component guides](https://wingo-ui.com/blog/category/components)
- Reading time: 8 min
- Canonical: https://wingo-ui.com/blog/react-stepper-component

## TL;DR

A React stepper component shows where someone is in a fixed sequence: an ordered list where each step is complete, current, upcoming, failed or loading, with aria-current="step" on the current one. Use numbered circles for forms, bars for checkout, dots for onboarding and a vertical list when steps need descriptions. Below about 640px a horizontal stepper should collapse into a "Step 2 of 4" header with a progress bar, which is what the Wingo UI Steps component does in CSS.

A React stepper component answers one question at a glance: how far along am I, and what is left? Checkouts, onboarding flows, identity checks (KYC) and long settings forms all use one. The hard parts are the states (what does a failed or loading step look like?), the layout (five steps with titles do not fit a phone) and accessibility (a row of circles means nothing to a screen reader without the right markup). This guide covers each with the [Steps](https://wingo-ui.com/components/steps) component from Wingo UI, the library we build, and compares it with the main alternatives. If the stepper sits on top of a form, the [React multi step form tutorial](https://wingo-ui.com/blog/react-multi-step-form) covers validation, saved drafts and transitions.

## When should you use a stepper?

Use one when the task has a fixed order, three to seven steps, and a clear end. Each step should depend on the one before it, or at least read better in that order: shipping before payment, ID before proof of address.

Skip it in two cases. If the sections are independent and people jump between them freely, use [Tabs](https://wingo-ui.com/components/tabs); a stepper implies an order that is not there. If there is only one long step, a plain [Progress](https://wingo-ui.com/components/progress) bar is enough.

## Which stepper variant fits which flow?

Numbered circles fit forms, bars fit checkout and dots fit onboarding. In Steps you pick one with `variant`:

| Variant | Looks like | Good for |
| --- | --- | --- |
| `circles` (default) | Numbered markers joined by connectors, titles under or beside them | Forms and wizards, account setup |
| `bars` | One segment per step with the title under it | A checkout stepper, short flows with short titles |
| `dots` | Small dots, the current one widened | Onboarding carousels, three to five slides |

Two layout props change the rest. `orientation="vertical"` stacks the steps in a column with descriptions beside the markers. A vertical stepper fits KYC and account opening, where each step needs a line of explanation ("A bank statement or utility bill"), and it works at any width. `labelPlacement="end"` puts horizontal titles beside the markers instead of under them.

Then the visual props every Wingo UI component shares: `size` (`sm`, `md`, `lg`: 24, 28 or 32px markers), `tone` (neutral by default, or brand, success, warning, danger, info), `color` for any CSS color, and `connector="dashed"` to draw the connectors still ahead as dashes.

```bash
npx wingo-ui@latest add steps
```

> Live demo (Steps): Click Continue and the connector fills toward the next step, the marker pops and the check draws in. Then click a completed step to go back to it. On a phone the same stepper shows the compact Step 2 of 4 header. Try it at [Steps](https://wingo-ui.com/components/steps) and install it with `npx wingo-ui@latest add steps`.

## What does each step state mean?

Every step is in one of five states. Steps derives three from the current `value`: steps before it are complete, the one at it is current, the ones after it are upcoming. The other two are always explicit, set with `status` on the step:

| State | Marker | Screen reader hears | Set by |
| --- | --- | --- | --- |
| `complete` | A check that draws itself in | "Completed" | Position before `value`, or `status` |
| `current` | Filled with the tone, with a soft ring | "Current step", plus `aria-current="step"` | Position at `value` |
| `upcoming` | The number (or `icon`) on a light tint | Nothing extra | Position after `value` |
| `error` | A red "!" | "Needs attention" | `status: "error"` |
| `loading` | A spinner | "In progress" | `status: "loading"` |

Errors and loading are explicit because only your code knows them. A KYC flow is the clearest case: the identity check runs on a server, can take seconds, and can fail for a reason the person can fix.

```tsx
"use client";

import { useState } from "react";
import { Steps, type StepItem } from "@/components/ui/steps";
import { Button } from "@/components/ui/button";

type Check = "idle" | "checking" | "failed" | "passed";

export function IdentityVerification({ verify }: { verify: () => Promise<boolean> }) {
  const [check, setCheck] = useState<Check>("idle");

  const steps: StepItem[] = [
    { id: "personal", title: "Personal details", description: "Emma Carter" },
    {
      id: "identity",
      title: "Identity check",
      description:
        check === "failed" ? "The photo is blurry. Take it again." : check === "checking" ? "Checking your ID" : "Driver's license or passport",
      status: check === "checking" ? "loading" : check === "failed" ? "error" : undefined,
    },
    { id: "address", title: "Proof of address", description: "A bank statement or utility bill" },
    { id: "done", title: "Done" },
  ];

  const send = async () => {
    setCheck("checking");
    setCheck((await verify().catch(() => false)) ? "passed" : "failed");
  };

  return (
    <div className="flex flex-col gap-5">
      <Steps steps={steps} value={check === "passed" ? "address" : "identity"} orientation="vertical" clickable="none" />
      {check !== "passed" && (
        <Button loading={check === "checking"} loadingText="Checking" onClick={send}>
          {check === "failed" ? "Upload the photo again" : "Send the photo"}
        </Button>
      )}
    </div>
  );
}
```

Put the reason for an error in the step's `description`, as above. A red "!" tells people that something is wrong; the text tells them what to do. `optional: true` shows an "Optional" note under the title (dots, which have no visible title, add it for screen readers only), and `disabled: true` keeps a step from ever being clickable.

## Which steps should be clickable?

`clickable` decides which steps are buttons. The default, `"completed"`, makes completed steps, the current one and failed ones clickable, which covers the common "go back and fix the address" case without letting anyone skip ahead. `"all"` makes every enabled step a button, for flows where order is a suggestion. `"none"` turns the stepper into a pure indicator, as in the KYC example, where only the server moves you forward.

Clicks call `onValueChange(id, step)`. A checkout stepper usually spans several routes, so the URL is the source of truth: give each step an `href` and pass your router's link. Steps renders clickable steps as links and the current one gets `aria-current="step"`:

```tsx
"use client";

import Link from "next/link";
import { usePathname } from "next/navigation";
import { Steps, type StepItem } from "@/components/ui/steps";

const CHECKOUT: StepItem[] = [
  { id: "cart", title: "Cart", href: "/checkout/cart" },
  { id: "shipping", title: "Shipping", href: "/checkout/shipping" },
  { id: "payment", title: "Payment", href: "/checkout/payment" },
  { id: "review", title: "Review", href: "/checkout/review" },
];

export function CheckoutProgress() {
  const pathname = usePathname();
  const current = CHECKOUT.find((step) => step.href === pathname)?.id ?? "cart";
  return <Steps steps={CHECKOUT} value={current} variant="bars" linkComponent={Link} />;
}
```

With the default `clickable`, the upcoming steps stay plain text, so nobody lands on the payment page with an empty cart. Guard the routes on the server too; a stepper is not access control.

One pitfall: comparing `usePathname()` with `href` breaks as soon as the path carries a locale prefix or a trailing slash. If the stepper renders from `app/checkout/layout.tsx`, `useSelectedLayoutSegment()` from `next/navigation` returns `"shipping"` directly, and you can match it against the step `id` instead.

## How does a stepper collapse on a phone?

Below 640px, a horizontal circles or bars stepper turns into a compact header: "Step 2 of 4" in small text, the current title, "Next: Payment" on the right and a thin progress bar under them. Both layouts are in the HTML and a CSS breakpoint picks one, so the server render is already right on a phone and nothing shifts after hydration. `mobileVariant="same"` keeps the full stepper on phones, and `compact` shows the compact header at every width, for a narrow card or a sheet.

Vertical steppers and dots keep their layout at every size, since both already fit a narrow screen. When a horizontal list is wider than its container and you opted out of the compact header, it scrolls sideways and keeps the current step in view.

On touch screens, clickable markers and dots get a 44px hit area, the minimum we use for every control. The reasoning is in [our touch target size post](https://wingo-ui.com/blog/minimum-touch-target-size-react-tailwind).

## How do you make a stepper accessible?

Use an ordered list, mark the current step with `aria-current="step"`, put each step's state in words and support the arrow keys. In Steps the markup looks like this:

- **An ordered list.** Steps renders an `<ol>` named "Progress" (`labels.progress`), so a screen reader announces a list of four items and the position of each.
- **`aria-current="step"`** on the current step, the attribute value the ARIA spec defines for exactly this case.
- **Status in words.** Each step's accessible name includes its state ("Shipping, Completed"), because color and a check icon are invisible to a screen reader.
- **Keyboard.** Tab reaches each clickable step; Left and Right (Up and Down when vertical) move between them, Home and End jump to the ends.
- **The compact header** is a `progressbar` with `aria-valuenow` and "Step 2 of 4" as its text value.

All of that copy is in `labels`, so a translation or a different tone of voice is one prop.

## What about a full checkout stepper in React?

For a complete checkout instead of only the indicator, the [Checkout Steps](https://wingo-ui.com/components/checkout-steps) block puts four steps (delivery details, shipping, payment and review) beside a sticky order summary. It validates each step, shows lockers and pickup points, adds payment fees to the total, and turns each step into a full-height page on a phone with the total and Continue above the home indicator. Its header uses the [Form Wizard](https://wingo-ui.com/components/form-wizard) stepper, `"horizontal"` or `"progress"`.

Be aware of its defaults: it was built for the Romanian market, so `country` is `"RO"` (postal code and company tax ID checks), `currency` is `"RON"`, and the Company switch asks for a CUI. Set `country`, `currency` and `invoice={false}` for other markets.

> Live demo (Checkout steps): Pick a shipping method and press Continue: the stepper marks the step done and the summary total updates. On a narrow screen the summary folds into a bar at the top. Try it at [Checkout steps](https://wingo-ui.com/components/checkout-steps) and install it with `npx wingo-ui@latest add checkout-steps`.

## How does it compare with MUI and shadcn?

MUI ships a mature stepper inside Material UI, shadcn/ui has none, and Wingo UI Steps is the one with a built-in phone header and error and loading states. The details, as of October 2026 (Material UI v9.5 docs):

| | Wingo UI Steps | MUI Stepper | shadcn/ui |
| --- | --- | --- | --- |
| Install | `npx wingo-ui@latest add steps` | `@mui/material` package | No official stepper; community registries such as ReUI |
| Looks | circles, bars, dots | Horizontal and vertical steppers, plus a separate MobileStepper (text, dots, progress) | Depends on the registry |
| States | complete, current, upcoming, error, loading | Active, completed, optional and an error example | ReUI: active, completed, inactive, loading |
| Phone layout | Compact header below 640px, automatic | Vertical stepper or MobileStepper, chosen by you | Up to you |
| Styling | Tailwind v4 tokens, source in your repo | MUI's styling system | Tailwind, source in your repo |

Sources: [MUI Stepper docs](https://mui.com/material-ui/react-stepper/), [shadcn/ui component list](https://ui.shadcn.com/docs/components), [ReUI stepper docs](https://reui.io/docs/stepper).

MUI is the better pick if your app already uses Material UI: its stepper follows the rest of that design system, and its docs say vertical steppers "are designed for narrow screen sizes." If you want a shadcn stepper that installs with the shadcn CLI, ReUI's composed API (`Stepper`, `StepperItem`, `StepperIndicator`) fits that style. Wingo UI Steps suits a Tailwind stepper that should arrive with the states, the phone header and the keyboard support already built.

## Where should you start?

Start with the [Steps docs](https://wingo-ui.com/components/steps): the playground and examples for vertical steps with icons, a loading and error flow, bars, dots, linked steps and the phone header. Steps, Checkout Steps and Form Wizard are part of Wingo UI Pro: $8 a month, $80 a year or $150 once as of October 2026 ([pricing](https://wingo-ui.com/pricing)). More guides like this one live in the [component guides category](https://wingo-ui.com/blog/category/components), starting with the [React form components guide](https://wingo-ui.com/blog/react-form-components-guide).

## Components in this post

- [Steps](https://wingo-ui.com/components/steps) (Pro): A React stepper for checkouts, onboarding and KYC: circles, bars or dots, with complete, error and loading states and a phone header. Install: `npx wingo-ui@latest add steps`
- [Checkout steps](https://wingo-ui.com/components/checkout-steps) (Pro): A React multi-step checkout: address and invoice, delivery with lockers, payment and a review that places the order, beside a summary. Install: `npx wingo-ui@latest add checkout-steps`

## FAQ

### What is a stepper component in React?

It is a progress indicator for a task split into ordered steps, such as a checkout or a sign-up. It shows which steps are done, which one is current and what is left, and it can let people jump back to a completed step.

### Does shadcn/ui have a stepper component?

No. As of October 2026 the official shadcn/ui component list has no stepper. Community registries such as ReUI publish one that installs with the shadcn CLI, and you can also build one from an ordered list and Tailwind classes.

### How do I make a React stepper vertical?

Stack the steps in a column with the markers on one side and the titles and descriptions beside them, and join the markers with a vertical connector. In Wingo UI Steps that is orientation="vertical", which also works at any screen width.

### How should a stepper look on mobile?

A row of numbered circles with titles rarely fits a 360px screen. Show a compact header instead: the step count, the current title, what comes next and a progress bar, or switch to a vertical list when people need to see every step.

### Should completed steps in a stepper be clickable?

Usually yes: going back to a completed step to fix something is the most common reason to click a stepper. Jumping forward past steps that were never filled in should stay blocked unless the steps are truly independent.

---

Source: https://wingo-ui.com/blog/react-stepper-component
