# Stepperize
> Typed, headless, framework-agnostic step/wizard state for React. One typed
> definition drives a hook, a provider, exhaustive rendering, and unstyled
> accessible primitives — all sharing the same step ids. Packages:
> `@stepperize/react` for React apps, `@stepperize/core` for the
> framework-agnostic types and pure helpers.
This file helps AI agents generate **correct, current Stepperize code**. The API
below is current. Older patterns (`stepper.navigation.next()`,
`stepper.flow.switch()`, `stepper.metadata`, `Scoped`, `defineStepper(stepA, stepB)`,
`stepper.render()`, `stepper.values`, `stepper.complete()`, `beforeChange`,
`navigation: "linear"`) are outdated and must not be used.
## Install
```bash
npm install @stepperize/react
```
## Canonical usage
```tsx
import { defineStepper } from "@stepperize/react";
// 1. Define once, at module scope. Pass an ARRAY of steps. `id` is required;
// every other field is yours and stays typed. Add `schema` (any Standard
// Schema) to type a step's flow data and enable validate(). The array must
// be non-empty and ids must be unique. Literal duplicate ids are TypeScript
// errors when possible; runtime validation catches dynamic arrays.
const checkout = defineStepper(
[
{ id: "shipping", title: "Shipping" },
{ id: "payment", title: "Payment" },
{ id: "review", title: "Review" },
],
{ defaultStep: "shipping", linear: false }, // options are optional
);
// 2. Use the flat instance.
function Checkout() {
const stepper = checkout.useStepper();
return (
{stepper.current.title}
{/* match is exhaustive — one handler per id, type-checked */}
{stepper.match({
shipping: () => ,
payment: () => ,
review: () => ,
})}
);
}
```
## The flat instance (returned by useStepper)
State: `steps`, `current`, `id`, `index`, `count`, `progress` (0..1),
`completed`, `isFirst`, `isLast`, `canPrev`, `canNext`, `isPending`.
Step access lives on the DEFINITION, not the instance: `checkout.get(id)`,
`checkout.at(index)`, `checkout.parseStep(value)`, or the raw `stepper.steps`
array. (The instance has no `indexOf`/`has`/`first`/`last`/`nextStep`/`neighbors`.)
Render/status: `match({ [id]: (step) => ReactNode })`, `status(id)` →
`"active" | "previous" | "upcoming"`, `is(id)`.
Navigation (all async, resolve to `boolean` "did it change"): `next(payload?)`,
`prev(payload?)`, `goTo(id, payload?)`, `reset(payload?)`, `canGoTo(id)`.
Payload: `{ data?: unknown }` — staged for the CURRENT step before the guard
runs, then committed only if the move is accepted. `goTo` bypasses the `linear`
policy. Always `await` when branching on result.
Flow data (committed cross-step data, not live field state): `data.get(id?)`,
`data.set(value)` / `data.set(id, value)`, `data.all()`, `data.clear(id?)`,
`data.reset()`. With a per-step `schema`, `data.get(id)` is typed as the schema input.
Validation hierarchy:
- `ctx.validate()` inside `beforeStepChange` validates the step being left against
the transition data snapshot, including any pending `next({ data })` payload.
This is the preferred guard API.
- `stepper.validate(id?)` validates STORED draft data from `stepper.data`.
- `checkout.validate(id, value)` (on the definition) validates an ARBITRARY value
against a step schema.
All return `Promise<{ success: true; data } | { success: false; issues }>`.
Schema is any Standard Schema (Zod/Valibot/ArkType; no runtime dep). Schemaless
steps always succeed with the value unchanged. NOTE: `stepper.data.get(id)` is the
DRAFT (schema input, may be invalid); validation returns the parsed OUTPUT — don't
treat draft data as validated.
Completion (explicit business state, separate from status):
`setComplete(id?, value = true)`, `isComplete(id?)`.
Lifecycle is options, not instance methods. There are no event subscriptions —
use a `useEffect` on `stepper.id` for ad-hoc effects.
## Definition object (returned by defineStepper)
`{ steps, useStepper(options?), Provider, Stepper, get(id), at(index), parseStep(value), validate(id, value) }`.
## Options (defineStepper 2nd arg = defaults; useStepper/Provider/Root arg = per-instance overrides)
- defineStepper: `defaultStep`, `defaultData`, `defaultCompleted`, `linear` (boolean).
Lifecycle callbacks are NOT accepted here — they are instance-only.
- useStepper / Provider / Stepper.Root: `defaultStep`, `step` + `onStepChange`
(controlled step; `step` may be a raw external string and `onStepChange`
receives known step ids), `onInvalidStep`, `defaultData`, `data` + `onDataChange`,
`completed` + `onCompletedChange`, `linear`, `beforeStepChange`.
- Controlled rule: if you pass `step`, you MUST pass `onStepChange` or it won't move
(same for `data`/`onDataChange`, `completed`/`onCompletedChange`).
## Lifecycle (every imperative navigation)
payload → `beforeStepChange(ctx)` (return `false` to cancel; can be async; sees
pending data; `ctx.validate()` validates the step being left) → commit (save data
+ change step) → `onStepChange(step, ctx)` → resolves `true`/`false`. `ctx`:
`{ from, to, fromIndex, toIndex, direction, data, validate, statuses }`,
`direction` ∈ `"next" | "prev" | "goto" | "reset"`. The guard runs on
imperative navigation only — external controlled `step` changes are authoritative
and do not run it.
## Validate before advancing (common pattern)
```tsx
const checkout = defineStepper([
{ id: "shipping", schema: ShippingSchema },
{ id: "review" },
]);
function Checkout() {
const stepper = checkout.useStepper({
beforeStepChange: async ({ direction, validate }) => {
if (direction === "prev") return true;
// validate() checks the step being left against this transition's data
// snapshot, including any pending next({ data }) payload.
return (await validate()).success; // false cancels
},
});
// submit + validate + complete in one action:
// const accepted = await stepper.next({ data: form.getValues() });
// if (accepted) stepper.setComplete();
}
```
## Shared state
Wrap with the generated `Provider`; descendants call `checkout.useStepper()` and
read the SAME instance (outside a provider the hook makes a local instance).
```tsx
```
## Primitives (unstyled, accessible) — `checkout.Stepper.*`
`Root` (provider-backed; render prop `{({ stepper }) => ...}`; `linear`,
`orientation`, all useStepper options), `List` (tablist + arrow-key nav,
`orientation`), `Items` (`{(step, index) => ...}`), `Item` (`step` prop, or auto
inside `Items`), `Trigger` (selects the item's step), `Title`, `Description`,
`Indicator`, `Separator`, `Content` (`step` prop; renders only when active),
`Actions`, `Prev`, `Next`. Most accept `render={(props) => ...}`; it replaces the
primitive root element — SPREAD `props`. Style via
`data-status="active|previous|upcoming"` and `data-component`. With `linear`,
`Trigger` and `List` keyboard navigation follow `canGoTo(id)`.
## Rules for generating Stepperize code
1. Pass a non-empty steps ARRAY to `defineStepper`. Define at module scope and keep ids unique.
2. The instance is FLAT: `stepper.next()`, not `stepper.navigation.next()`.
3. Use `stepper.match({...})` (exhaustive), not `stepper.render` or `flow.switch`.
4. `data` for per-step flow data (not `values`/`metadata`); `Provider` for sharing (not `Scoped`).
5. `await` navigation when you need the boolean result.
6. Controlled `step` requires `onStepChange`.
7. Status is positional; completion is explicit (`setComplete()` / `isComplete()`).
8. Lifecycle is `beforeStepChange` (guard) + `onStepChange` (after), passed to the hook/provider.
9. Navigation policy is `linear: boolean`, not `navigation: "linear" | "non-linear"`.
## Docs
- Quick start: /docs/latest/getting-started/first-stepper
- The stepper instance (live): /docs/latest/core-concepts/stepper-instance
- Navigation lifecycle: /docs/latest/core-concepts/navigation-lifecycle
- Status vs completion: /docs/latest/core-concepts/status-vs-completion
- Controlled vs uncontrolled: /docs/latest/core-concepts/controlled-vs-uncontrolled
- Primitives: /docs/latest/guides/primitives
- Building forms (patterns, not libraries): /docs/latest/forms
- Form patterns (drafts, validation gate, review, edit, persist): /docs/latest/forms/patterns
- Schema & validate() (Standard Schema, draft vs validated): /docs/latest/forms/schema-validation
- Form implementations: /docs/latest/forms/{react-hook-form,tanstack-form,conform}
- Reference: /docs/latest/api/react/stepper-instance
- Production checklist: /docs/latest/production/checklist
- Migrate v6 → latest: /docs/latest/migration/v7
```