Hook
Access and control your stepper with the useStepper hook
The useStepper hook returns the stepper instance with a structured API: state, navigation, lookup, flow, metadata, and lifecycle. Inside <Scoped> or Stepper.Root, useStepper() (with no arguments) reads from context. In a component with no provider, useStepper() creates its own stepper state for that component. You can still pass config when outside a provider to set initialStep and initialMetadata.
The hook accepts an optional single argument: a config object with:
initialStep— The ID of the initial step (must be one of your step IDs).initialMetadata— Partial record of step ID → metadata; used to seedmetadata.get(id)andstate.current.metadataper step.
Usage
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ();
return (
<>
<>{....}</>
< ={() => ..()} ={..}>Next</>
< ={() => ..()} ={..}>Previous</>
</>
);
};Rendering methods
The stepper exposes flow with helpers to render content by step: flow.is, flow.when, flow.switch, and flow.match.
when
The flow.when method allows rendering content conditionally based on the current step. It takes a step ID (string or array of ID plus conditions), a whenFn (the function to run if the step matches), and an optional elseFn (run when it does not match).
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ();
return (
<>
{..("first", () => (
<>First step: {.}</>
))}
{..("second", () => (
<>Second step: {.}</>
))}
</>
);
};You can pass an optional elseFn to render when the current step does not match:
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "info", : "Info" },
{ : "review", : "Review" },
{ : "done", : "Done" }
);
function () {
const = ();
return (
<>
{..(
"review",
() => <>You're on review: {.}</>,
() => <>Other step: {.}</>
)}
</>
);
}You can also use an array as the first argument: the first element is the step ID, the rest are boolean conditions. The step matches only when the ID matches and all conditions are true. This allows multi-condition logic (e.g. step + feature flag or validation state).
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ();
const = true;
const = true;
return (
<>
{..(["first", , ], () => (
<>First step (editable): {.}</>
))}
{..("second", () => (
<>Second step: {.}</>
))}
</>
);
};switch
The flow.switch method lets you render content based on the current step ID in a switch-case style. It is a cleaner and more scalable way to handle many steps without multiple when conditions.
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ();
return (
<>
{..({
: () => <>First: {.}</>,
: () => <>Second: {.}</>,
})}
</>
);
};match
The flow.match method lets you render content based on an external step ID (e.g. from the server, URL, or other state). Unlike flow.switch, which uses the stepper’s current step, flow.match takes that ID as the first argument. Useful for frameworks like Remix with server-side state or when driving the UI from a URL param.
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ();
const = "first"; // e.g. from server or URL
return (
<>
{..(, {
: () => <>First: {.}</>,
: () => <>Second: {.}</>,
})}
</>
);
};is
For simple conditionals, use flow.is(id). It returns true when the current step’s ID equals id.
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "form", : "Form" },
{ : "confirmation", : "Confirmation" }
);
function () {
const = ();
return (
<>
{..("confirmation") && <>Order summary here.</>}
{..("form") && <>Fill the form.</>}
</>
);
}State and navigation
| Property / Method | Description |
|---|---|
state.all | Array of all step objects |
state.current | { data, index, status, metadata } — current step; use state.current.metadata.get/set/reset for current step only |
state.isFirst / state.isLast | true when on first or last step |
state.isTransitioning | true while a transition (with lifecycle callbacks) is in progress |
navigation.next() | Go to next step |
navigation.prev() | Go to previous step |
navigation.goTo(id) | Go to step by ID |
navigation.reset() | Reset to initial step |
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "info", : "Info" },
{ : "review", : "Review" },
{ : "done", : "Done" }
);
function () {
const = ();
return (
<>
<>Step {... + 1}: {....}</>
< ="button" ={() => ..()} ={..}>Back</>
< ="button" ={() => ..()} ={..}>Next</>
< ="button" ={() => ..("review")}>Jump to Review</>
< ="button" ={() => ..()}>Reset</>
</>
);
}Lookup
Step lookup helpers (same shape as generateStepperUtils from @stepperize/core):
| Method | Description |
|---|---|
lookup.getAll() | All steps |
lookup.get(id) | Step object by ID |
lookup.getIndex(id) | Index of step by ID |
lookup.getByIndex(index) | Step at index |
lookup.getFirst() / lookup.getLast() | First / last step |
lookup.getNext(id) / lookup.getPrev(id) | Next / previous step after id |
lookup.getNeighbors(id) | { prev, next } for step id |
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "info", : "Info" },
{ : "review", : "Review" },
{ : "done", : "Done" }
);
function () {
const = ();
const = ..("review");
const = ..(....);
return (
<>Review: {?.}; neighbors: {.?. ?? "none"} → {.?. ?? "none"}</>
);
}Transition hooks (before / after)
For actions before or after a transition (e.g. validation, analytics), use stepper.lifecycle.onBeforeTransition(cb) and stepper.lifecycle.onAfterTransition(cb). Multiple callbacks are supported (e.g. one at the Stepper level, one per step); each call returns an unsubscribe function. They run on every next(), prev(), and goTo(). Return false (or a promise that resolves to false) from onBeforeTransition to cancel the transition.
The callback receives a TransitionContext with: from, to, metadata, statuses, direction ("next" | "prev" | "goTo"), fromIndex, toIndex. Use direction to branch (e.g. run logic only before "next" or "prev").
To avoid stale metadata in the hook when you set metadata and then call next(), prev(), or goTo() in the same handler, pass a payload into navigation: next({ metadata: { "step-id": { ... } } }), prev({ metadata: ... }), or goTo(id, { metadata: ... }). That metadata is merged into ctx.metadata for the transition and persisted when the transition completes. If an onBeforeTransition callback returns false and cancels the transition, the payload (including any metadata) is not persisted — the step index and metadata state stay as they were. If you need to commit metadata even when the transition is cancelled, call stepper.metadata.set(id, values) (or stepper.state.current.metadata.set(values)) inside the before callback.
onBeforeTransition
Runs before the step index updates. Return false to cancel.
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First" },
{ : "second", : "Second" }
);
function () {
const = ();
React.(() => {
const = ..(async () => {
if (. === "prev" && !.("Go back?")) return false;
});
return () => ();
}, []);
return (
< ="button" ={() => ..()} ={..}>
Next
</>
);
}Multiple callbacks (Stepper + step) and unsubscribe
You can register one handler at the Stepper level and another inside a step component; all run in registration order. If any onBeforeTransition callback returns false, the transition is cancelled. Each call returns an unsubscribe function — use it in your useEffect cleanup so the handler is removed when the component unmounts (e.g. when navigating away from that step).
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "step-1", : "Step 1" },
{ : "step-2", : "Step 2" }
);
function () {
const = ();
React.(() => {
const = ..(async () => {
if (. === "prev" && !.("Go back?")) return false;
});
return () => ();
}, []);
return (
<>
{..({
"step-1": () => < />,
"step-2": () => < />,
})}
</>
);
}
function () {
const = ();
React.(() => {
const = ..(async () => {
if (.. === "step-1") {
// Step-specific validation; both this and the global handler run
const = await ();
if (!) return false;
}
});
return () => ();
}, []);
return (
< ="button" ={() => ..()}>
Next
</>
);
}
function () {
return <>Step 2</>;
}
async function (): <boolean> {
return true;
}Passing metadata in the transition (avoid stale ctx)
If you set metadata and then call next() in the same handler, onBeforeTransition still sees the previous metadata (setState is async). Pass a payload so the hook receives fresh data. The same payload works for next(payload), prev(payload), and goTo(id, payload):
const handleNext = async () => {
const url = await generateUrl();
stepper.navigation.next({ metadata: { "step-1": { ...step1Meta, data: { url } } } });
};
stepper.navigation.prev({ metadata: { "step-1": { reason: "back" } } });
stepper.navigation.goTo("summary", { metadata: { "payment": { method: "card" } } });onAfterTransition
Runs after the step index has updated.
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First" },
{ : "second", : "Second" }
);
function () {
const = ();
React.(() => {
const = ..(() => {
.(`${..} → ${..}`);
});
return () => ();
}, []);
return (
< ="button" ={() => ..()} ={..}>
Next
</>
);
}Metadata
Metadata lets you attach custom data per step (e.g. form drafts, values from the server). You can set initial metadata in the hook config and read/write it at runtime.
Initial metadata
Pass initialMetadata in useStepper({ ... }) to seed metadata per step:
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" }
);
const = () => {
const = ({
: {
: { : "1" },
},
});
const = ..("first");
return <>First step: {?.value}</>;
};set / get / reset
metadata.set(id, values)— Set metadata for a step.metadata.get(id)— Get metadata for a step.metadata.reset(keepInitialMetadata?)— Reset all metadata; passtrueto keep initial values from config.
Current step only: state.current.metadata.get(), state.current.metadata.set(values), state.current.metadata.reset().
import React from "react";
import { } from "@stepperize/react";
const { } = (
{ : "first", : "First step" },
{ : "second", : "Second step" },
{ : "last", : "Last step" }
);
const = () => {
const = ();
const = () => {
..("first", { : "saved" });
};
const = ..("first");
return (
<>
< ="button" ={}>Save</>
<>First step: {?.value}</>
< ="button" ={() => ..(true)}>Reset metadata</>
</>
);
};API reference
| Name | Type | Description |
|---|---|---|
state | StepperState<Steps> | all, current (data, index, status, metadata), isFirst, isLast, isTransitioning |
navigation | StepperNavigation<Steps> | next(payload?), prev(payload?), goTo(id, payload?), reset() — payload can include metadata to merge into transition context |
lookup | StepperLookup<Steps> | getAll(), get(id), getIndex(id), getByIndex(index), getFirst() / getLast(), getNext(id) / getPrev(id), getNeighbors(id) |
flow | StepperFlow<Steps> | is(id), when(id, whenFn, elseFn?), switch(when), match(state, matches) |
metadata | StepperMetadata<Steps> | values, set(id, values), get(id), reset(keepInitialMetadata?) |
lifecycle | { onBeforeTransition, onAfterTransition } | Register callbacks (multiple supported); each returns unsubscribe; return false from onBeforeTransition to cancel |
Last updated on