Form Wizard
Multi-step form with typed step ids, per-step validation gates, progress, and persisted drafts.
| Package | @vizejs/ui/form-wizard |
| Maturity | stable |
| Own the source | vize lib pull form-wizard |
| Requires | context, controllable-state, id |
| Aliases | multi-step form, wizard, checkout steps, onboarding flow |
| Covers | WAI-ARIA aria-current=step, Chakra Steps, Ark UI Steps |
Minimal setup
Use a Vue 3.5+ project with Vue SFC compilation configured (see Vite+ integration).
vp install @vizejs/ui
Add the public import below to <script setup lang="ts">. Components provide behavior without styles; the preview uses optional Paper styles.
import { FormWizard, FormWizardStep, FormWizardNext, FormWizardBack, FormWizardProgress } from "@vizejs/ui/form-wizard";
Or copy the source into your project with vize lib pull form-wizard (see Source Distribution).
Try it
This is the basic example maintained with the component source, using the public package imports. The preview adds the optional Paper styles.
Open the interactive example · Styling these components
Copy the example
<!-- Three-step sign-up wizard with a controlled step, a validation gate, and native progress. -->
<script setup lang="ts">
import { ref, useId } from "vue";
import {
FormWizard,
FormWizardBack,
FormWizardNext,
FormWizardProgress,
FormWizardStep,
} from "@vizejs/ui/form-wizard";
import type { FormWizardValidationContext } from "@vizejs/ui/form-wizard";
type SignupStep = "account" | "profile" | "review";
const steps: readonly SignupStep[] = ["account", "profile", "review"];
const current = ref<SignupStep>("account");
const email = ref("");
const displayName = ref("");
const completed = ref(false);
const emailId = useId();
const nameId = useId();
function validate({ step }: FormWizardValidationContext<SignupStep>): boolean {
return step !== "account" || email.value.includes("@");
}
function finish(): void {
completed.value = true;
}
</script>
<template>
<FormWizard v-model="current" :steps :validate aria-label="Create account" @complete="finish">
<FormWizardProgress />
<FormWizardStep v-slot="{ index }" step="account" label="Account">
<h3>Step {{ index + 1 }}: Account</h3>
<label :for="emailId">Email</label>
<input :id="emailId" v-model="email" type="email" autocomplete="email" required />
</FormWizardStep>
<FormWizardStep v-slot="{ index }" step="profile" label="Profile">
<h3>Step {{ index + 1 }}: Profile</h3>
<label :for="nameId">Display name</label>
<input :id="nameId" v-model="displayName" autocomplete="nickname" />
</FormWizardStep>
<FormWizardStep v-slot="{ index }" step="review" label="Review">
<h3>Step {{ index + 1 }}: Review</h3>
<p>{{ displayName || "Anonymous" }} <{{ email }}></p>
</FormWizardStep>
<FormWizardBack>Back</FormWizardBack>
<FormWizardNext v-slot="{ isLast }">{{ isLast ? "Create account" : "Next" }}</FormWizardNext>
<output>{{ completed ? "Account created" : `On step: ${current}` }}</output>
</FormWizard>
</template>
API
FormWizard<StepId extends string>
Source: src/families/form/form-wizard/form-wizard.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
steps (required) |
readonly StepId[] |
required |
Step ids in order; their literal union types every step-related API. |
modelValue |
StepId |
undefined |
Controlled current step. undefined selects uncontrolled behavior. |
defaultValue |
StepId |
steps[0] |
Initial uncontrolled step. |
validate |
FormWizardValidate<StepId> |
undefined |
Validation gate run before leaving a step forward and before completing. |
linear |
boolean |
true |
Only allow jumping forward to visited steps (or the next one). |
draft |
FormWizardDraftStore<StepId> |
undefined |
Persisted draft store for the current and visited steps. |
focusOnChange |
boolean |
true |
Move focus to the newly current step panel after a user transition. |
id |
string | null |
undefined |
Base id for panels (<id>-<step>). null and undefined select a deterministic fallback. |
ariaLabel |
string |
undefined |
Accessible name of the wizard region. |
ariaLabelledby |
string |
undefined |
Ids that label the wizard region. |
Events
| Event | Payload | Description |
|---|---|---|
update:modelValue |
[step: StepId] |
Fired when the current step requests a change. |
change |
[step: StepId, previous: StepId, direction: FormWizardDirection] |
Fired after the current step changes, with the previous step and direction. |
blocked |
[step: StepId, target: StepId] |
Fired when a validation gate keeps the wizard on a step. |
complete |
[] |
Fired when the last step passes its gate. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
FormWizardSlotState<StepId> |
Renders step panels, navigation, and progress with the typed wizard state. |
Exposed
| Member | Type | Description |
|---|---|---|
steps |
readonly StepId[] |
Every step id, in order. |
current |
StepId |
Current step. |
index |
number |
Zero-based index of the current step. |
count |
number |
Number of steps. |
progress |
number |
Completed fraction from 0 to 1 (index / (count - 1), 1 for a single step). |
visited |
readonly StepId[] |
Steps reached so far, in step order. |
isFirst |
boolean |
Whether the current step is the first. |
isLast |
boolean |
Whether the current step is the last. |
validating |
boolean |
Whether a validation gate is running. |
completed |
boolean |
Whether the last step passed its gate and complete was emitted. |
state |
FormWizardState |
Stable state token. |
next |
() => Promise<boolean> |
Validate the current step, then advance (or complete on the last step). |
back |
() => boolean |
Go to the previous step without validation. |
goTo |
(step: StepId) => Promise<boolean> |
Go to a step; moving forward validates every step in between. |
reset |
() => void |
Return to the first step, forget visited steps, and clear the draft. |
root |
— |
FormWizardStep
Source: src/families/form/form-wizard/form-wizard-step.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
step (required) |
string |
required |
Step id this panel belongs to; must be one of the wizard steps. |
label |
string |
undefined |
Accessible name of the step panel, for example its heading text. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly active: boolean; readonly index: number } |
Step contents; rendered for every step so field state survives navigation. |
FormWizardNext
Source: src/families/form/form-wizard/form-wizard-next.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly isLast: boolean; readonly validating: boolean } |
Button contents; isLast lets consumers switch between "Next" and "Finish". |
FormWizardBack
Source: src/families/form/form-wizard/form-wizard-back.vue
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly isFirst: boolean } |
Button contents. |
FormWizardProgress
Source: src/families/form/form-wizard/form-wizard-progress.vue
Props
| Prop | Type | Default | Description |
|---|---|---|---|
formatValueText |
(step: number, count: number) => string |
(step, count) => |
Builds the announced value text from the 1-based step number and total. |
ariaLabel |
string |
"Progress" |
Accessible name of the progress bar. |
Slots
| Slot | Slot props | Description |
|---|---|---|
default |
{ readonly step: number; readonly count: number; readonly progress: number; } |
Optional fallback contents rendered inside the native progress element. |
Behavior
Normative state x input -> outcome table for form-wizard.vue,
form-wizard-step.vue, form-wizard-next.vue, form-wizard-back.vue, and
form-wizard-progress.vue (@vizejs/ui/form-wizard). steps infers the step-id
union used by v-model, gates, events, and the expose API. Every row is proven
by the named test in form-wizard.test.ts or form-wizard-ssr.test.ts;
inference is pinned in form-wizard.types.test-d.ts.
| # | State | Input | Outcome | Proven by |
|---|---|---|---|---|
| W1 | first step | render | labelled role="group"; every panel renders (hidden unless current, aria-current="step"); Back disabled; native progress "Step 1 of 3" |
renders every step panel with only the current one visible |
| W2 | any | Next / Back | Next advances (or emits complete on the last step), Back returns; change(step, previous, direction) fires and focus moves to the new panel |
next and back move between steps, focus the panel, and complete on the last step |
| W3 | gated | Next | the gate receives { step, target, signal }; while pending the root is aria-busy and Next is disabled; false keeps the step and emits blocked; Back never validates |
validation gates block forward moves and report the blocked step |
| W4 | linear | goTo |
backward jumps are free; forward jumps need visited targets (unless linear=false) and validate every step in between |
goTo jumps back freely, forward only through visited steps, validating in between |
| W5 | draft store | mount / change / complete / reset | the draft loads after mount (unknown steps ignored), saves { step, visited } after each change, and clears on completion and reset |
drafts restore after mount, save every change, and clear on completion or reset |
| W6 | controlled | Next | emits the request; the rendered step follows modelValue |
controlled steps win until the parent accepts them |
| W7 | invalid setup | empty steps / missing root |
throws VIZE_UI_FORM_WIZARD_STEPS or VIZE_UI_CONTEXT_MISSING: FormWizard |
rejects empty step lists and parts outside a FormWizard |
| W8 | SSR / hydration | isolated requests | the server renders the default step; drafts apply only after hydration, so markup matches and no mismatch is reported | renders the default step on the server and applies drafts only after hydration |
Public extension contract
| Surface | Contract |
|---|---|
| Parts | root, step (panel), next, back, progress. |
| Data attributes | root data-state (in-progress/validating/complete) and data-step; panel data-state (active/visited/upcoming). |
| CSS properties | --vize-form-wizard-progress on the progress element (0%–100%). |
| Integration | Gate each step with useForm validation (for example validate: () => form.validate().then((r) => r.valid)). |
The subpath is tree-shakable and ships no CSS; those package contracts are
pinned by distribution.test.ts, check:size, and check:tree-shaking.