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.
Account
Tell us who you are
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.
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
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.
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
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
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 inferredstepper.match({…})— exhaustive, type-checked renderingstepper.next()/prev()— flat, ergonomic navigation
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.
Expense #2043
$1,250 · Travel
What should I write?
Expense #2043
$1,250 · Travel
Submitted
Manager
Finance
Approved
Ship your first flow in minutes.
Install the package, define your steps, and render. The flat, typed API gets out of your way.