Skip to content

Wizard

Terminal window
$ npx shadcn@latest add https://lacodda.github.io/dowel/r/wizard.json
See it liveA workspace-setup wizard that checks each step, waits on a slow check, keeps what was typed on Back - above and beside its stepper.→

The Stepper on top, the current step’s content under it, and a Back / Next / Finish row. It installs the Stepper with it.

<Wizard
stepperLabel={t('setup.steps')}
stateLabels={{ done: t('step.done'), error: t('step.error') }}
summary={(position, total) => t('step.position', { position, total })}
backLabel={t('back')}
nextLabel={t('next')}
finishLabel={t('setup.create')}
onFinish={createWorkspace}
steps={[
{ id: 'name', label: t('setup.name'), canAdvance: () => name !== '', content: <NameStep /> },
{ id: 'storage', label: t('setup.storage'), onNext: checkFolder, content: <StorageStep /> },
{ id: 'review', label: t('setup.review'), content: <Review /> },
]}
/>

Clicking a step. Clicking a step behind you is free. Clicking a completed step ahead runs every check between here and there, and stops on the first step that fails. The Stepper’s default reach applies, so steps never reached cannot be clicked.

A step counts as done only once it passes. When you go back, a step you have seen but never completed stays upcoming. It is not a way forward.

Every step is checked in the same order. Next, Enter in a field, and a click on a step ahead all run the same checks: the browser’s own constraints on the step’s fields (required, pattern), then canAdvance, then the async onNext. If any returns false or throws, the user stays on the step and it is marked failed. While onNext runs, the controls are disabled, so a second press does not run it twice. A thrown error goes to onError, or is rethrown if there is none, so it is never swallowed. The form is noValidate because the browser would otherwise check the hidden fields of earlier steps too.

Enter is Next, never Finish. Enter in a field goes through the same checks as Next. On the last step Enter does nothing: finishing creates something, so it takes a press of the button that says so.

Focus moves to the new step’s heading. When the step changes, the keyboard lands on the step’s h2, which is focusable by script only. Without that, focus would stay on the Next button with a new page above it, and a screen reader would say nothing. The Wizard does not take focus when it first renders.

Back keeps what was typed. A step’s panel stays mounted once visited and is hidden rather than unmounted. Going Back finds text, choices and toggles as they were left, whether or not the product lifted that state out.

Every word is the product’s. The button labels and the stepper’s words are required props. There are no English defaults.

Controlled or not. defaultStep covers the common case. step with onStepChange is for a step kept in the URL or restored from storage. A controlled product decides whether to move. It never decides whether to check: onStepChange is called only after the checks pass.

A server can fail a step from outside. A step’s error marks it failed when something the wizard cannot see rejected what it holds.

Props

Read from the types in ui/wizard.tsx, the file a product copies - so the description of each prop is also what an editor shows over it there.

PropTypeDefault
stepsrequiredreadonly WizardStep[]The steps, in order. Each has an id, a label, its content, and optional checks that run before the wizard moves on.
stepstringThe step shown, for a controlled wizard.
defaultStepstringThe step shown first, for an uncontrolled one. The first step if omitted.
onStepChange(id: string) => voidCalled with the step to move to - after its checks have passed, when moving forward. A controlled wizard moves when the product sets step.
onFinishrequired() => void | Promise<void>Called when Finish is pressed on the last step and its checks pass.
onError(error: unknown, stepId: string) => voidCalled when a step's onNext throws. The step is blocked and marked failed either way; without this the error is rethrown so it is not lost.
stepperLabelrequiredstringThe stepper's name for a reader.
stateLabelsrequiredStepStateLabelsThe words for a step's state - done, failed, and optionally not yet reached - which the stepper says to a screen reader beside the mark.
summary(position: number, total: number) => ReactNodeThe position in words, such as (2, 5) => 'Step 2 of 5'. The stepper shows it when it is too narrow for its labels.
backLabelrequiredReactNodeThe buttons' words. Required: the component has none of its own.
nextLabelrequiredReactNodeThe words of the button that goes to the next step.
finishLabelrequiredReactNodeThe words of the button on the last step that finishes the wizard.
orientationhorizontal | vertical'horizontal'Stepper above the content, or beside it.
level1 | 2 | 3 | 4 | 5 | 62Where a step's title stands in the page's outline: 2 by default, under the screen's title.

And every prop of React's HTMLAttributes<HTMLFormElement> except children, onSubmit, onError, passed on.

Accessibility

The examples on the stand, through axe 4.14.0 in a browser, in both themes, against WCAG 2.2 A and AA and axe's best practices: 19 rules applied and passed, and none failed. The accessibility report has every item.

History

Each release that changed ui/wizard.tsx - the file a product copies - newest first.

  1. v0.41.0
    • docs read every props table from the component's types ee3b159
  2. v0.38.0
    • feat a heading stands at the level the page gives it 101c183
  3. v0.31.0
    • feat progress through steps, and the form that walks them 8c95cac