- ab"shipping"
- ab"payment"
- ab"review"
Your step ids are a type
Ids become a literal union the moment you call defineStepper. Typos and missing cases fail in your editor, not in production. No generics to wire up.
Define your steps once. Stepperize infers every id, guards every move, and tells you exactly why a navigation failed, while your markup, forms, and design system stay yours.
$ npm i @stepperize/reactconst checkout = defineStepper([ { id: "account", title: "Account" }, { id: "shipping", title: "Shipping" }, { id: "review", title: "Review" }, { id: "done", title: "Done" },]); function Checkout() { const stepper = checkout.useStepper({ async beforeStepChange({ from, direction }) { if (direction === "prev") return true; if (from.id === "account") return isEmail(email); if (from.id === "review") await placeOrder(); }, }); return stepper.match({ account: () => <Account />, shipping: () => <Shipping />, review: () => <Review />, done: () => <Done />, });}Account
Where we send the receipt
Live: type an email, step through, then place the order. Every line on the left is what drives the panel on the right.
Why Stepperize
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.
}More than a stepper
Most stepper libraries stop at "which step is active". Stepperize handles the hard parts too: guards that block, computed branching, and async transitions with retry. These demos are live.
Built for production
One typed definition powers navigation, rendering, validation and state, without dictating a single line of your markup.
Ids become a literal union the moment you call defineStepper. Typos and missing cases fail in your editor, not in production. No generics to wire up.
Navigation resolves to a typed result. Check accepted, then handle the exact reason a move was refused.
Each useStepper() is its own flow. A Provider shares one, and selectors keep re-renders focused.
Unstyled Stepper.* components ship roles, ARIA and keyboard navigation. Style them through data attributes.
Attach a Standard Schema per step and keep fields in React Hook Form, TanStack Form, Conform, or plain state.
Learn moreOwn your UI
Headless core, optional unstyled primitives, and production-ready blocks built with Tailwind and shadcn/ui. Install one from the registry and it is your code, with nothing to eject.
Install the package, define your steps, render. The flat, typed API gets out of your way.
$ npm i @stepperize/react