Stepperize v8Explore the changes
v8 · See what’s new

The type-safe way to build multi-step experiences in React.

Define your steps once. Keep state local or share it across components, validate before moving, and handle every navigation result with typed confidence. Your markup, your forms, your flow.

Current npm release

Using v7? Review the changes before upgrading.

GitHub starsnpmdownloadsTinygzippedZerodependencies

Account

Tell us who you are

1/4

Full name

Email address

Your fields render here — Stepperize only tracks the flow.

Inside v8

A clearer API for every flow.

Migrating from v7

Choose where state lives

Each useStepper() call creates a local flow. A Provider shares a flow through useStepperContext(), with selectors for focused subscriptions.

Try local and shared flows

Know how every move ends

Navigation returns a typed result. Check accepted, then handle the reason when a guard, a boundary, or another rule prevents the move.

Explore navigation results

Save and complete together

Pass data and complete to one navigation action. They commit only when the move is accepted. Reset restores the flow’s initial state.

See the instance API

Why Stepperize

Stop hand-rolling step state

Every multi-step flow starts with an index, a switch, and clamping logic — then quietly rots as steps and rules pile up. Stepperize replaces all of it with one typed instance.

Without Stepperize
checkout.tsx
const steps = ["shipping", "payment", "review"];
const [step, setStep] = useState(0);

// clamp by hand, every time
const next = () => setStep((s) =>
  Math.min(s + 1, steps.length - 1));

// stringly-typed — typo still compiles
switch (steps[step]) {
  case "shiping": return <Shipping />;  // 💥
  case "payment": return <Payment />;
  // forgot review? no warning.
}
  • Manual index + clamping on every move
  • Stringly-typed switch — a typo still compiles
  • Miss a case and nothing warns you
  • Validation gating wired by hand, per step
With Stepperize
checkout.tsx
const checkout = defineStepper([
  { id: "shipping" },
  { id: "payment" },
  { id: "review" },
]);

const stepper = checkout.useStepper();

// call from your Next button
const onNext = () => stepper.next();

// exhaustive — a typo is a compile error
return stepper.match({
  shipping: () => <Shipping />,
  payment: () => <Payment />,
  review: () => <Review />,
});
  • next() / prev() clamp and guard for you
  • Step ids are a typed union — typos fail to compile
  • match() is exhaustive — miss a case, get an error
  • Add a guard when navigation needs validation

More than a stepper

Real flow logic, not just step tracking

Most stepper libraries stop at "which step is active." Stepperize handles the hard parts too — guards that block navigation, computed branching, and async transitions with retry. These blocks are live: click through them.

Validation that blocks

A failed schema check makes the guard return false — the transition is cancelled, not just frowned upon.

Where should we ship?

Branching paths

An earlier answer rewrites the path. A personal account skips the team step entirely.

Step 1 of 4

How will you use the app?

Async + retry

The transition awaits real work, exposes isPending, fails the first try, and recovers on retry.

Provision a database

Type-safe, proven

Your step ids are a type

Step ids become a literal union the moment you call defineStepper. Navigation, rendering, and per-step data are checked against it — so these mistakes fail in your editor, not in production.

A mistyped id won't compile

stepper.ts
const stepper = checkout.useStepper();

stepper.goTo("paymnt");
//      ^ did you mean "payment"?

Argument of type "paymnt" is not assignable to parameter of type "shipping" | "payment" | "review". ts(2345)

match() must be exhaustive

stepper.ts
return stepper.match({
  shipping: () => <Shipping />,
  payment: () => <Payment />,
  // review is required — forgot it?
});

Property review is missing in type { shipping; payment; } but required in the matcher. ts(2741)

No generics to wire up. Inference flows from your step array.

A tiny, obvious API

Define once. Drive anything.

One typed definition powers independent local flows and shared providers. Choose useStepper() for local state or useStepperContext() to read a provider. Step ids stay inferred through navigation, rendering, and primitives.

  • defineStepper — your steps, fully inferred
  • stepper.match({…}) — exhaustive, type-checked rendering
  • useStepperContext(selector) — subscribe to the state you need
Explore the instance, live
checkout.tsx
import { defineStepper } from "@stepperize/react";

const checkout = defineStepper([
  { id: "shipping", title: "Shipping" },
  { id: "payment",  title: "Payment"   },
  { id: "review",   title: "Review"    },
]);

function Checkout() {
  const stepper = checkout.useStepper();

  // exhaustive — TypeScript checks every step id
  return stepper.match({
    shipping: () => <Shipping />,
    payment:  () => <Payment />,
    review:   () => <Review />,
  });
}

One small mental model

Stepperize owns the flow. You own everything else.

It does one thing well — manage the flow — and stays out of the way of your UI and your data.

Your UI owns presentation

Render anything — your markup, your design system, or the bundled primitives.

Stepperize owns the flow

Active step, navigation, guards, drafts, and completion — one flat instance.

Your data owns its state

Keep field state, validation, and domain logic wherever it already lives.

Everything you'd expect

Built for production, not demos

Flow logic built in

Guards, branching, async transitions, completion state, and schema validation — from one stepper instance.

Type-safe end to end

Step ids become a literal union. Navigation, rendering, and values are all checked at compile time.

Headless by default

Zero styles, zero markup opinions. Compose it into any UI or design system you already use.

Form-library agnostic

Pairs cleanly with React Hook Form, TanStack Form, Conform, Zod — or nothing at all.

Accessible primitives

Stepper.* components ship roles, ARIA, and keyboard nav — ready for your styles.

React 18 & 19

Server-render your flow, then hydrate it on the client. Use client components in React Server Component apps.

Own your UI

No lock-in. It's your code.

Stepperize is headless, with optional unstyled primitives. Start from 31 copy-paste blocks built with primitives and Tailwind, paste them into your project, and own every line. No theme to fight, nothing to eject.

Multi-step formsOnboardingCheckoutSetup wizardsSurveysAI workflowsApproval chainsDecision trees
Approval timeline

Expense #2043

$1,250 · Travel

Requester
AI workflow
Copy Assistant

What should I write?

Approval flow

Expense #2043

$1,250 · Travel

Pending
  1. Submitted

  2. Manager

  3. Finance

  4. Approved

Waiting on Submitted approval.

Ship your first flow in minutes.

Install the package, define your steps, and render. The flat, typed API gets out of your way.