Skip to content

Wizards

useWizard(steps, options?) is a composable for multi-step forms built on top of defineMachine. The wizard machine is generated automatically from the steps array.

ts
function useWizard<TContext>(
  steps: WizardStep<TContext>[],
  options?: WizardOptions,
): WizardInstance<TContext>

Wizard steps

WizardStep fields:

id

string. Unique step identifier (becomes a state name internally).

label

string?. Display label.

component

Component?. Vue component to render for this step.

canProceed

(ctx) => boolean | Promise<boolean>. Gate for next() and forward goTo(); may be async. Receives the wizard's real, live context — whatever onEnter/onLeave have merged into it so far.

onEnter

(ctx) => void | Partial<TContext>. Called when the wizard enters this step. Return a partial to merge it into context — e.g. seed a default.

onLeave

(ctx) => void | Partial<TContext>. Called when the wizard leaves this step. Return a partial to merge it into context — the usual place to persist a step's collected data (e.g. form fields) before canProceed on the next step reads it.

Wizard options

WizardOptions fields:

id

string? · default: auto-generated. Machine id registered in the MachineStore (when VueMachinePlugin is installed) and shown in DevTools. Set this explicitly if you need a stable, predictable id (e.g. to look the wizard up via useMachineStore().get(id)); otherwise each useWizard() gets its own unique id automatically.

initialStep

number · default: 0. Index of the starting step.

allowSkip

boolean · default: false. Skip canProceed on forward goTo().

circular

boolean · default: false. next() wraps from last step back to first.

Return value

currentStep

Ref<WizardStep>. Currently active step object.

currentIndex

ComputedRef<number>. Zero-based index of the current step.

totalSteps

number. Total number of steps.

progress

ComputedRef<number>. 0 to 1 based on current index.

isFirst

ComputedRef<boolean>. true on the first step.

isLast

ComputedRef<boolean>. true on the last step.

history

Ref<string[]>. IDs of visited steps.

context

Readonly<Ref<TContext>>. Accumulated context — whatever onEnter/onLeave have merged in so far.

next()

Promise<boolean>. Advance; calls canProceed first; returns false if blocked.

prev()

void. Go back (no guard).

goTo(id)

Promise<boolean>. Jump to step by id; respects canProceed unless allowSkip.

reset()

void. Return to the initial step.

Example

vue
<script setup lang="ts">
import { useWizard } from '@macrulez/vue-state-machine'
import type { WizardStep } from '@macrulez/vue-state-machine'
import StepInfo from './StepInfo.vue'
import StepAddress from './StepAddress.vue'
import StepPayment from './StepPayment.vue'

interface CheckoutCtx {
  name: string
  email: string
  address: string
}

const steps: WizardStep<CheckoutCtx>[] = [
  {
    id: 'info',
    label: 'Your info',
    component: StepInfo,
    // Populated by StepInfo's own onLeave below before this ever runs on
    // the *next* step's canProceed check — see "context" in the return
    // value above.
    canProceed: (ctx) => !!ctx.name && !!ctx.email,
  },
  {
    id: 'address',
    label: 'Delivery',
    component: StepAddress,
    canProceed: (ctx) => !!ctx.address,
    // Return a partial to merge collected form data into context — e.g. read
    // from a ref StepAddress updates via v-model, or from an emit it fires.
    onLeave: (): Partial<CheckoutCtx> => ({ address: addressFieldRef.value }),
  },
  {
    id: 'payment',
    label: 'Payment',
    component: StepPayment,
    onEnter: () => trackEvent('payment_step_entered'),
  },
]

const { currentStep, context, progress, isFirst, isLast, next, prev } = useWizard(steps)
</script>

<template>
  <div>
    <progress :value="progress" max="1" />

    <component :is="currentStep.component" :context="context" />

    <nav>
      <button :disabled="isFirst" @click="prev">Back</button>
      <button v-if="!isLast" @click="next">Next</button>
      <button v-else @click="submit">Place order</button>
    </nav>
  </div>
</template>

Rules for canProceed

  • May return a boolean or a Promise<boolean>
  • If it returns false, next() / forward goTo() return false and the wizard stays on the current step
  • If it throws, the same outcome — false is returned, the error is logged to console.error in dev mode
  • prev() and backward goTo() never check canProceed
  • allowSkip: true disables canProceed for goTo() only; next() always checks it