FAQ

Find quick answers to common Stepperize questions.

FAQ

Is Stepperize a form library?

No. Stepperize owns the flow — which step is active, how you move between steps, and optional per-step drafts. Keep field state, validation, and errors in your form library (React Hook Form, Conform, TanStack Form). They compose well: see Building forms with Stepperize.

Does it ship any styles or markup?

No. The flat instance (useStepper) is logic only. The Stepper.* primitives render unstyled elements with accessibility wiring and data-* attributes, so you bring your own CSS. Browse ready-made looks in the Blocks gallery.

How is status different from completion?

stepper.status(id) is positional and derived from the current index: "active", "previous", or "upcoming". stepper.isComplete(id) is business state you set explicitly with setComplete(id) / setComplete(id, false). A step the user walked past is "previous", but it is not completed until you say so.

Do I need @stepperize/core?

Not for React apps. @stepperize/react depends on it and re-exports the common types. Reach for core only when you need its pure helpers (createStepMap, matchStep, the status helpers) in non-React code such as loaders, tests, or adapters. See Core utilities.

When should I use Provider vs Stepper.Root vs plain useStepper?

UseWhen
useStepper()One component owns the whole flow.
checkout.ProviderSeveral components share one instance, but you write your own markup.
checkout.Stepper.RootYou want shared state and the accessible primitive components.

All three produce the same flat instance. More in Shared state.

How do I sync the current step to the URL?

Make the step controlled and drive it from your router:

const stepper = checkout.useStepper({
  step: stepFromUrl,
  onStepChange: (next) => navigate({ search: { step: next } }),
  onInvalidStep: () => navigate({ search: { step: "shipping" }, replace: true }),
});

The controlled step option accepts raw URL strings. If the value is unknown, Stepperize falls back to the default step and calls onInvalidStep. Use parseStep when you need to narrow a raw value before passing it to another typed API.

How do I validate a step before moving on?

Return false from beforeStepChange. It runs before the change commits and can be async, so you can validate against a schema. Validation that blocks navigation is a transition guard, and beforeStepChange is the transition guard.

const stepper = checkout.useStepper({
  beforeStepChange: async ({ direction, validate }) => {
    if (direction === "prev") return true;
    return (await validate()).success;
  },
});

Pass the value in the same call so the guard can see it before React re-renders: await stepper.next({ data: form.getValues() }).

What does navigation return?

A boolean (wrapped in a Promise): true if the step changed, false if it did not — at an edge, an unknown id, an async change already running, the same step without a value payload, or cancelled by beforeStepChange. await it when you need the result.

Can I run side effects after a step change?

Yes. Use the onStepChange option for flow-wide effects, or a plain effect on stepper.id for component-level reactions:

React.useEffect(() => track(stepper.id), [stepper.id]);

How do I handle the final "submit" step?

Stepper.Next disables itself on the last step (canNext is false). Render your own button for the final action and key it off stepper.isLast. See Troubleshooting.

Can the same step appear twice?

No. Ids must be unique — they are the key for navigation, values, completion, rendering, and primitives. Literal duplicates are TypeScript errors when possible, and defineStepper also throws at runtime if an id appears more than once.

Does it work with Next.js / SSR / React Server Components?

Yes. Keep defineStepper(...) at module scope; it has no side effects and is safe to import on the server. useStepper and the primitives are client APIs, so any component that uses them must be a Client Component ("use client").

Which React versions are supported?

React 17, 18, and 19. It runs in plain JavaScript too, but you lose the type-safety that makes the typed ids worthwhile.

How do I reset the whole flow?

stepper.reset() returns to the initial/default step. Reset drafts and completion separately with stepper.data.reset() and setComplete(id, false), since those are independent business state.

I'm migrating from v6 — what changed?

The instance is now flat (stepper.next() instead of stepper.navigation.next()), steps are passed as an array, metadata became data, and Scoped became Provider. Full mapping in Migrating to v7.

Edit on GitHub

Last updated on

On this page