Skip to content

Wizard ​

Split a long form into ordered steps. Steps validate as the user moves through them, a stepper tracks progress and lets you jump to any step, and a final review page recaps every answer with edit links before submit.

What It Demonstrates ​

  • The wizard metadata property: the marked node's children become the steps, in declaration order
  • A non-linear wizard (allowForwardJump): next() and stepper jumps surface each step's errors but never block, so the user can move freely and fix things in any order
  • A stepper that reflects the current step and jumps to any step, forward or back
  • A wizardSummaryPage step that reviews the collected values with per-section edit links
  • submitButtonText for the label on the final step's submit button

Example ​

Event Registration

Reserve your spot

Attendee

Who is attending?

Ticket

Choose what you are booking.

Preferences

Optional extras, skip anything that does not apply.

Review & submit

Check everything before you register. Jump to any step to make a change.

Attendee

Full name
—
Email
—

Ticket

Type
—
Quantity
—

Preferences

Dietary
—
Newsletter
No

Ready to submit?

We'll email your ticket and a calendar invite to confirm your spot.

IsDirty: false
Touched: false
Valid: true

// form values:
{
  "attendee": {},
  "ticket": {},
  "preferences": {}
}
    

How It Works ​

Mark a node with wizard. Its children become the steps, each one a heading group with its own fields. This example passes wizard: { allowForwardJump: true } so the user can move through the steps freely; the last step is a wizardSummaryPage that shows a read-only recap:

ts
const metadata: Metadata[] = [
  {
    name: 'registration',
    path: '',
    wizard: { allowForwardJump: true },
    fieldOptions: { label: formName },
    description: 'Reserve your spot',
    submitButtonText: 'Complete registration',
    children: [
      {
        name: 'attendee',
        type: 'heading',
        fieldOptions: { label: 'Attendee' },
        description: 'Who is attending?',
        helpText: 'Your details',
        children: [
          {
            name: 'fullName',
            fieldOptions: { label: 'Full name' },
            restriction: { minLength: 2 },
            fullWidth: true,
          },
          {
            name: 'email',
            fieldOptions: { label: 'Email' },
            restriction: { pattern: '^.+@.+\\..+$' },
            fullWidth: true,
          },
        ],
      },
      {
        name: 'ticket',
        type: 'heading',
        fieldOptions: { label: 'Ticket' },
        description: 'Choose what you are booking.',
        helpText: 'Pick a ticket',
        children: [
          {
            name: 'ticketType',
            type: 'select',
            fieldOptions: { label: 'Ticket type' },
            options: ticketOptions,
          },
          {
            name: 'quantity',
            type: 'select',
            fieldOptions: { label: 'Quantity' },
            options: [
              { key: '1', value: '1' },
              { key: '2', value: '2' },
              { key: '3', value: '3' },
            ],
          },
        ],
      },
      {
        name: 'preferences',
        type: 'heading',
        fieldOptions: { label: 'Preferences' },
        description: 'Optional extras, skip anything that does not apply.',
        helpText: 'Fine-tune',
        children: [
          {
            name: 'dietary',
            type: 'select',
            minOccurs: 0,
            fieldOptions: { label: 'Dietary requirements' },
            options: dietaryOptions,
          },
          {
            name: 'newsletter',
            type: 'checkbox',
            minOccurs: 0,
            fieldOptions: { label: 'Send me event updates by email' },
            fullWidth: true,
          },
        ],
      },
      {
        name: 'summaryPage',
        type: 'wizardSummaryPage',
        fieldOptions: { label: 'Review & submit' },
        description: 'Check everything before you register. Jump to any step to make a change.',
        helpText: 'Confirm',
        wizardSummaryConfirmation: 'We\'ll email your ticket and a calendar invite to confirm your spot.',
        minOccurs: 0,
        computedProps: [(field) => { field.wizardSummary = wizardSummary.value; }],
      },
    ],
  },
];

The engine hands the template all step state and navigation (currentStepIndex, pages, isFirst, isLast, next(), prev(), gotoStep()) as slot props on the -wizard / -wizard-page slot family. The docs template forwards these to its FormWizard and Stepper components, so the wizard chrome lives entirely in template code, not in the library. See Wizard container and page slots for the full slot contract.

Pages must stay mounted

Wizard page slots gate visibility with v-show, never v-if. Every page stays mounted at all times so its fields keep their values and validation when the user navigates away. A v-if would clear that page's data (unless keepValuesOnUnmount is set) and, regardless of that setting, deregister its fields from vee-validate, so a later submit could send the page's data unvalidated even when its values survived. Because keepValuesOnUnmount preserves only the values and not the field registration, v-show is still required for any page that has fields.

Linear vs non-linear ​

The example above is non-linear because it sets allowForwardJump. Without it, wizard: true gives a linear wizard: next() blocks on an invalid step, and the stepper only jumps backward to already-visited steps, so the user must complete each step in order before moving on.

ts
{
  name: 'registration',
  wizard: true, // linear: next() blocks, stepper is backward-only
  children: [/* ... */],
}

allowForwardJump makes forward movement non-blocking: both next() and forward jumps from the stepper surface the current step's errors but let the user continue anyway. Validation still fires so errors stay visible; to move without validating at all, add validateOnJump: false for a silent jump.

The Review Page ​

The final step is a wizardSummaryPage. A computedProps function feeds it a wizardSummary array, and the template renders one review group per section with an edit link that calls gotoStep():

ts
const wizardSummary = computed<ReviewGroupProps[]>(() => [
  {
    title: 'Attendee',
    rows: [
      ['Full name', values?.attendee?.fullName || '—'],
      ['Email', values?.attendee?.email || '—'],
    ],
  },
  {
    title: 'Ticket',
    rows: [
      ['Type', ticketLabel.value || '—'],
      ['Quantity', values?.ticket?.quantity || '—'],
    ],
  },
  {
    title: 'Preferences',
    rows: [
      ['Dietary', dietaryLabel.value || '—'],
      ['Newsletter', values?.preferences?.newsletter ? 'Subscribed' : 'No'],
    ],
  },
]);

Handling Submit ​

The final step's button is a native type="submit" control, so the wrapping form forwards its submit event to your own handleSubmit from useDynamicForm:

ts
function reset() {
  resetForm();
  submitted.value = undefined;
}

// The wizard's last-page button is a native type="submit" control, so AdvancedForm forwards the
// form's own submit event here for handleSubmit to take over.
function onSubmit(event?: Event) {
  handleSubmit(
    (values) => {
      submitted.value = removeNullValues(values);
    },
    () => {},
  )(event);
}

For a production-scale wizard that layers conditional fields, dynamic options, repeatable steps, and a choice-based branch on top of these basics, see the Client Onboarding Wizard.

Released under the MIT License.