useWizard
The Wizard state machine without chrome: steps, values, validation, completion, and navigation.
Signature
useWizard provides wizard state and navigation without rendering a layout. Pass it to WizardProvider for WizardNav, WizardFooter, and WizardSummary, or connect it to custom controls.
function useWizard<T>(options: {
steps: WizardStep<T>[];
activeStep?: string; // controlled step id
defaultStep?: string;
onStepChange?: (id, step, meta: { index; direction }) => void;
values?: T; // controlled answers
defaultValues?: T;
onValuesChange?: (values: T) => void;
completed?: string[]; // controlled completion
defaultCompleted?: string[];
onCompletedChange?: (ids: string[]) => void;
enforceStepOrder?: boolean; // default true
validateOnNext?: boolean; // default true
onFinish?: (values: T) => void | Promise<void>;
onCancel?: () => void;
onValidationError?: (id: string, message: ReactNode) => void;
}): WizardApi<T>;Return value
| Prop | Type | Default | Description |
|---|---|---|---|
steps | WizardStep<T>[] | — | The steps in play, with includeWhen already applied. |
allSteps | WizardStep<T>[] | — | Every step handed to the wizard, hidden ones included. |
step | WizardStep<T> | — | The step being shown. |
index | number | — | Its position among the visible steps, zero-based. |
total | number | — | How many steps are in play. |
isFirst | boolean | — | True on the first step. |
isLast | boolean | — | True on the last step. |
direction | "forward" | "back" | "jump" | — | Which way the last move went; drives the content transition. |
progress | number | — | 0–100 by completed steps. What the progress bar draws. |
values | T | — | The answers so far. |
setValues | (patch | updater) => void | — | Merges a patch, or replaces via an updater. |
setValue | (key, value) => void | — | One key, for onChange handlers. |
completed | string[] | — | Ids of steps that have been passed. |
skipped | string[] | — | Ids of optional steps that were skipped. |
errors | Record<string, ReactNode> | — | Current message per step id. |
status | (id) => WizardStepStatus | — | current | complete | skipped | error | upcoming, for one step. |
validating | boolean | — | A step's validate is running. |
submitting | boolean | — | onFinish is in flight. |
canGoTo | (id: string) => boolean | — | Whether the nav should let the reader jump there. |
canAdvance | boolean | — | True when the step permits advancement and no validation or submission is pending. |
next | () => Promise<boolean> | — | Validates, marks the step complete, and moves on, or finishes on the last step. |
back | () => void | — | Steps back. Never validates. |
goTo | (target, options?) => Promise<boolean> | — | By id or by index among the visible steps. Refused when the step is not reachable. |
skip | () => void | — | Moves past an optional step without validating it. |
finish | () => Promise<boolean> | — | Runs onFinish, with submitting for as long as it takes. |
reset | () => void | — | Back to the first step, with the starting values. |
markComplete | (id: string) => void | — | Marks a step passed without visiting it. |
markIncomplete | (id: string) => void | — | Un-marks one; a later answer invalidated an earlier step. |
setError | (id, message | null) => void | — | Sets or clears a step's message from outside its validator. |
onCancel | (() => void) | undefined | — | Controls whether WizardFooter displays Cancel. |
Usage
Provider components
const wizard = useWizard({ steps, defaultValues: { region: "eu" } });
<WizardProvider value={wizard}>
<Card>
<CardHeader sub={`${wizard.index + 1} / ${wizard.total}`}>
Attach a cluster
</CardHeader>
<Box px="3" py="2" borderBottom bg="bg-deep">
<WizardNav orientation="horizontal" showDetails={false} />
</Box>
<CardBody>
{typeof wizard.step.content === "function"
? wizard.step.content(wizard)
: wizard.step.content}
</CardBody>
<WizardFooter />
</Card>
</WizardProvider>;Custom controls
WizardProvider is optional when using the API directly. next still validates and goTo still checks reachability.
<Button
variant="primary"
loading={wizard.validating || wizard.submitting}
disabled={!wizard.canAdvance}
onClick={() => void wizard.next()}
>
{wizard.isLast ? "Install" : "Continue"}
</Button>Resuming
A self-hosted setup that survives a container restart keeps values and completed outside the hook. Both are controllable, so persisting a flow takes two props and a defaultStep.
const saved = JSON.parse(localStorage.getItem("setup") ?? "{}");
const wizard = useWizard({
steps,
values: config,
onValuesChange: (next) => {
setConfig(next);
persist(next);
},
defaultCompleted: saved.completed,
defaultStep: saved.step,
onStepChange: (id) => persistStep(id),
});