Wizard
Guide people through a task with step validation and an async finish action.
import { Wizard, WizardStep } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a wizard when a task is long enough to split but each part depends on the one before: creating a workspace, a checkout, importing data. It draws a Stepper, shows one step's content, and handles Back, Continue, Skip and Finish. When every field fits on one screen, a single Form is quicker to fill in. When people need to compare the steps, don't hide them: lay them out as sections of one page.
import { Wizard, WizardStep } from '@youj/react'
export function CreateProject() {
return (
<Wizard onComplete={() => createProject(values)} finishLabel="Create project">
<WizardStep id="name" title="Name" validate={() => values.name.length > 0}>
<NameFields />
</WizardStep>
<WizardStep id="repo" title="Repository" optional>
<RepoPicker />
</WizardStep>
<WizardStep id="review" title="Review">
<Summary />
</WizardStep>
</Wizard>
)
}Each WizardStep is a step: its title goes in the stepper and heads the content, and its children show while it's current. The wizard keeps the step index itself, or pass step and onStepChange to control it. On the last step, Finish runs onComplete through the Button's onAction, so it shows a spinner and then a drawn check with finishSuccessLabel.
Description
A vertical, four-step workspace setup with required fields, a slug that's checked asynchronously and a final launch button.
The default, horizontal wizard puts a compact stepper above the content; below the sm breakpoint it shows only discs. orientation="vertical", as in the Demo story, puts the stepper in a column beside the content from sm up, which suits longer flows with descriptions.
A checkout wizard where the Payment step blocks Continue until a card number is entered.
validate runs when someone leaves a step forwards. Return true to move on, false to stay (the step's own field errors explain why), or a message, which appears under the step and is announced. It can be async: Continue shows a spinner while it runs. A failed step is marked in the stepper until it passes.
In the Demo story, the workspace name "acme" is taken, which the step finds out after a short async check.
Here, continuing past Payment with an empty card number returns a message instead of false. footerStart puts a Cancel button at the inline start of the footer, away from the forward actions.
Code
wizard-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propschildren, step, defaultStep… (18)
Props
| Prop | Type | |
|---|---|---|
childrenRequired | ReactNode | – |
step | numberControlled current step index. | – |
defaultStep | numberdefault 0 | 0 |
onStepChange | (step: number) => void | – |
onComplete | () => unknown | Promise<unknown>Runs when Finish is pressed on the last step (after its validation). Resolve for success. | – |
orientation | 'horizontal' | 'vertical'default 'horizontal' | 'horizontal' |
backLabel | stringLabels. | – |
nextLabel | string | – |
finishLabel | string | – |
finishSuccessLabel | string | – |
footerStart | ReactNodeExtra content in the footer, at the inline start (e.g. a Cancel button). | – |
headingLevel | 2 | 3 | 4The step title's heading level: 3 under a section or dialog title (the default), 2 straight under a page's h1. default 3 | 3 |
Other props pass through: extends ComponentProps<'div'>. | ||
WizardStep
| Prop | Type | |
|---|---|---|
idRequired | string | – |
titleRequired | ReactNode | – |
description | ReactNode | – |
optional | booleanCan be skipped: shows "Optional" and a Skip button. | – |
validate | () => ValidationResult | Promise<ValidationResult>Runs when leaving the step forwards. Return (or resolve) | – |
childrenRequired | ReactNode | – |
MotionStep enter, step exit, panel height… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Step enterdrifts 24px in from the side you travel to and fades in | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Step exitdrifts 24px out the other way as it fades | duration.baseease.standard220ms · cubic-bezier(0.2, 0, 0, 1) | 220mscubic-bezier(0.2, 0, 0, 1) | |
| Panel heightglides between the two steps' heights | spring.smooth0.36s, bounce 0 | 0.36s, bounce 0 | |
| Stepperthe connector fills or drains, see Stepper | duration.slowease.standard340ms · cubic-bezier(0.2, 0, 0, 1) | 340mscubic-bezier(0.2, 0, 0, 1) | |
| Finishthe button's spinner, then a drawn check | duration.slowease.emphasized340ms · cubic-bezier(0.6, 0, 0.1, 1) | 340mscubic-bezier(0.6, 0, 0.1, 1) |
Reduced motion
Steps swap in place and the panel takes its new height at once.
The drift follows the reading direction, so under RTL forwards comes in from the left. The outgoing step is popped out of the layout while it leaves, so rapid Back and Continue presses retarget the height without a jump.
Reduced motion
Steps replace each other in place with no drift or fade, and the panel height changes at once. Focus, announcements and validation are unchanged.
AccessibilityKeyboard, ARIA, focus
Accessibility
- "Step 2 of 4" above the content is a polite live region that also speaks the step's title, so each move is announced.
- After Back or Continue, focus moves to the new step's heading, so a screen reader starts reading from the top of the step and a keyboard user tabs on into its fields.
- A validation message has
role="alert", and the failed step is spoken as "has an error" in the stepper. - Done steps in the stepper are buttons that go back to them. Later steps can only be reached by Continue, so validation can't be skipped.
- Continue sets
aria-busywhile an async validation runs, and ignores presses until it settles.