shadcn-ready · copy-paste blocks

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

Define your steps once and get a fully-typed stepper — navigation, validation guards, branching, and async transitions included. Headless, so the UI stays yours.

GitHub starsnpmdownloadsTinygzippedZerodependencies

Account

Tell us who you are

1/4

Full name

Email address

Your fields render here — Stepperize only tracks the flow.

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", schema: shippingSchema },
  { id: "payment", schema: paymentSchema },
  { id: "review" },
]);

const stepper = checkout.useStepper();

// clamps + runs guards for you
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
  • Schema validation blocks the transition

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 a hook, a provider, and accessible primitives. Step ids flow through render, goTo, and every component — so a typo is a compile error, not a runtime surprise.

  • defineStepper — your steps, fully inferred
  • stepper.match({…}) — exhaustive, type-checked rendering
  • stepper.next() / prev() — flat, ergonomic navigation
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.

SSR & RSC ready

Works in the App Router and on the server. No window access, no hydration surprises.

Own your UI

No lock-in. It's your code.

Stepperize is headless — it ships logic, never markup. 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.