Wizard

Multi-step form patterns with numbered steps, tab navigation, and progress bar indicators. Guide users through complex forms with clear progress feedback.

Overview

Wizard patterns break complex forms into manageable steps, reducing cognitive load and improving completion rates. VB includes wizard.js, a production-ready controller that transforms standard HTML forms into multi-step wizards with progressive enhancement — forms still work without JavaScript.

Key features:

  • wizard.js controller — auto-initializes on form[data-wizard], handles step navigation, validation, and progress
  • nav.steps integration — auto-syncs step indicator state as the user navigates
  • Conditional steps (data-wizard-if) — show/hide steps based on user input
  • Optional steps (data-wizard-optional) — skip without validation
  • Programmatic API — wizardNext(), wizardPrev(), wizardGoTo(), wizardReset()
  • Custom events — wizard:stepchange, wizard:complete, wizard:reset
  • Visual patterns — numbered step indicators, tab navigation, and progress bars
  • Accessibility — aria-current="step", live regions, keyboard navigation

Numbered Steps

A horizontal step indicator with numbered circles connected by lines. Completed steps show checkmarks and can be clicked to navigate back. The current step is highlighted with the interactive color.

<body data-layout="cover" data-layout-min="100vh" data-layout-padding="l"> <layout-card data-layout-max="narrow" data-padding="l" data-layout-principal> <div data-layout="stack" data-layout-gap="xl"> <header data-layout="stack" data-layout-gap="s"> <h1>Create your account</h1> <p>Step 2 of 4 - Tell us about yourself</p> </header> <!-- Step Indicator — uses nav.steps from VB core --> <nav class="steps" data-labels="below" aria-label="Progress"> <ol> <li data-completed><a href="#account">Account</a></li> <li aria-current="step">Details</li> <li>Preferences</li> <li>Review</li> </ol> </nav> <!-- Step 2: Details --> <form action="/wizard/step2" method="POST" data-layout="stack" data-layout-gap="l"> <form-field> <label for="first-name">First name</label> <input type="text" id="first-name" name="first_name" required autocomplete="given-name" placeholder="John"/> </form-field> <form-field> <label for="last-name">Last name</label> <input type="text" id="last-name" name="last_name" required autocomplete="family-name" placeholder="Doe"/> </form-field> <div data-layout="cluster" data-layout-justify="between" data-layout-gap="m"> <button type="button" class="secondary">Back</button> <button type="submit">Next</button> </div> </form> </div> </layout-card> </body>

Step Indicator

The step indicator uses nav.steps from VB core — no custom CSS needed. See the Steps pattern for all variants and CSS variables.

/* No custom CSS needed — nav.steps is included in VB core. See the Steps pattern page for all variants and CSS variables. */

Tab Navigation

A tab-style navigation where each step is represented as a tab. Completed tabs can be clicked to jump back to previous sections. Future tabs are visually disabled until their prerequisites are met.

<body data-layout="cover" data-layout-min="100vh" data-layout-padding="l"> <layout-card data-layout-max="default" data-padding="none" data-layout-principal> <div data-layout="stack" data-layout-gap="none"> <!-- Tab Navigation --> <nav class="wizard-tabs" aria-label="Form steps"> <ol class="tab-list" role="tablist"> <li class="tab-item" role="presentation"> <button class="tab completed" role="tab" aria-selected="false" id="tab-account" aria-controls="panel-account"> <span class="tab-number"> <icon-wc name="check" size="xs"></icon-wc> </span> <span>Account</span> </button> </li> <li class="tab-item" role="presentation"> <button class="tab completed" role="tab" aria-selected="false" id="tab-profile" aria-controls="panel-profile"> <span class="tab-number"> <icon-wc name="check" size="xs"></icon-wc> </span> <span>Profile</span> </button> </li> <li class="tab-item" role="presentation"> <button class="tab active" role="tab" aria-selected="true" id="tab-preferences" aria-controls="panel-preferences" aria-current="step"> <span class="tab-number">3</span> <span>Preferences</span> </button> </li> <li class="tab-item" role="presentation"> <button class="tab future" role="tab" aria-selected="false" aria-disabled="true" id="tab-confirm" aria-controls="panel-confirm"> <span class="tab-number">4</span> <span>Confirm</span> </button> </li> </ol> </nav> <!-- Tab Panel --> <div class="tab-panel" role="tabpanel" id="panel-preferences" aria-labelledby="tab-preferences"> <header data-layout="stack" data-layout-gap="xs"> <h2>Set your preferences</h2> <p>Customize your experience with these settings.</p> </header> <form action="/wizard/preferences" method="POST" data-layout="stack" data-layout-gap="l"> <form-field> <label for="timezone">Timezone</label> <select id="timezone" name="timezone" required> <option value="">Select timezone...</option> <option value="est" selected>Eastern Time (ET)</option> </select> </form-field> <fieldset> <legend>Notifications</legend> <layout-stack data-layout-gap="s"> <label> <input type="checkbox" name="email_notifications" checked/> Email notifications for updates </label> </layout-stack> </fieldset> <div data-layout="cluster" data-layout-justify="between" data-layout-gap="m"> <button type="button" class="secondary">Back</button> <button type="submit">Continue to Review</button> </div> </form> </div> </div> </layout-card> </body>

Tab Navigation Styles

.wizard-tabs { border-bottom: var(--border-width-thin) solid var(--color-border); } .tab-list { display: flex; list-style: none; padding: 0; margin: 0; } .tab-item { flex: 1; } .tab { display: flex; align-items: center; justify-content: center; gap: var(--space-xs); padding: var(--space-m) var(--space-l); background: transparent; border: none; border-bottom: 3px solid transparent; color: var(--color-text-muted); font-weight: 500; width: 100%; margin-bottom: -1px; } .tab-number { width: 1.5rem; height: 1.5rem; border-radius: 50%; background: var(--color-surface-raised); border: 1px solid var(--color-border); display: inline-flex; align-items: center; justify-content: center; font-size: var(--text-xs); } /* Active tab */ .tab.active { color: var(--color-interactive); border-bottom-color: var(--color-interactive); } .tab.active .tab-number { background: var(--color-interactive); border-color: var(--color-interactive); color: white; } /* Completed tab */ .tab.completed { cursor: pointer; color: var(--color-text); } .tab.completed .tab-number { background: var(--color-success); border-color: var(--color-success); color: white; } /* Future tab */ .tab.future { opacity: 0.6; cursor: default; }

Progress Bar

A horizontal progress bar showing completion percentage with step dots below. Includes text showing "Step X of Y" and the current step name. Great for checkout flows and linear processes.

<body data-layout="cover" data-layout-min="100vh" data-layout-padding="l"> <layout-card data-layout-max="narrow" data-padding="l" data-layout-principal> <div data-layout="stack" data-layout-gap="xl"> <!-- Progress Bar --> <div class="wizard-progress" role="progressbar" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100" aria-label="Form completion progress"> <div class="progress-header"> <span class="progress-step">Step 2 of 4</span> <span class="progress-label">Payment Information</span> </div> <div class="progress-track"> <div class="progress-fill" style="width: 50%;"></div> </div> <div class="progress-percentage">50% complete</div> <div class="progress-dots" aria-hidden="true"> <div class="progress-dot"> <span class="dot completed"></span> <span class="dot-label">Cart</span> </div> <div class="progress-dot"> <span class="dot active"></span> <span class="dot-label">Payment</span> </div> <div class="progress-dot"> <span class="dot"></span> <span class="dot-label">Shipping</span> </div> <div class="progress-dot"> <span class="dot"></span> <span class="dot-label">Confirm</span> </div> </div> </div> <!-- Step 2: Payment --> <form action="/checkout/payment" method="POST" data-layout="stack" data-layout-gap="l"> <header data-layout="stack" data-layout-gap="xs"> <h1>Payment details</h1> <p>Enter your payment information securely.</p> </header> <form-field> <label for="card-number">Card number</label> <input type="text" id="card-number" name="card_number" required autocomplete="cc-number" placeholder="1234 5678 9012 3456"/> </form-field> <div data-layout="split" data-layout-gap="m"> <form-field> <label for="expiry">Expiry date</label> <input type="text" id="expiry" name="expiry" required autocomplete="cc-exp" placeholder="MM/YY"/> </form-field> <form-field> <label for="cvc">Security code</label> <input type="text" id="cvc" name="cvc" required autocomplete="cc-csc" placeholder="123"/> </form-field> </div> <div data-layout="cluster" data-layout-justify="between" data-layout-gap="m"> <button type="button" class="secondary"> <icon-wc name="arrow-left" size="sm"></icon-wc> Back to Cart </button> <button type="submit"> Continue to Shipping <icon-wc name="arrow-right" size="sm"></icon-wc> </button> </div> </form> </div> </layout-card> </body>

Progress Bar Styles

.wizard-progress { --progress-height: 0.5rem; --progress-bg: var(--color-border); --progress-fill: var(--color-interactive); } .progress-header { display: flex; justify-content: space-between; align-items: baseline; margin-bottom: var(--space-s); } .progress-step { font-weight: 600; color: var(--color-text); } .progress-label { font-size: var(--text-s); color: var(--color-text-muted); } .progress-track { width: 100%; height: var(--progress-height); background: var(--progress-bg); border-radius: var(--radius-full); overflow: hidden; } .progress-fill { height: 100%; background: var(--progress-fill); border-radius: var(--radius-full); transition: width var(--duration-normal) var(--ease-default); } .progress-percentage { font-size: var(--text-xs); color: var(--color-text-muted); margin-top: var(--space-xs); text-align: right; }

wizard.js Controller

VB includes wizard.js, a lightweight controller that enhances form[data-wizard] elements with step navigation, validation, conditional steps, and progress tracking. It works as a standard form without JavaScript (progressive enhancement).

Basic Setup

Add data-wizard to a form. Each <fieldset data-wizard-step> becomes a step. The controller auto-initializes on DOMContentLoaded.

<form data-wizard> <progress data-wizard-progress max="3" value="1"></progress> <fieldset data-wizard-step> <legend>Personal Information</legend> <!-- form fields --> </fieldset> <fieldset data-wizard-step> <legend>Account Details</legend> <!-- form fields --> </fieldset> <fieldset data-wizard-step> <legend>Confirmation</legend> <!-- form fields --> </fieldset> <nav data-wizard-nav> <button type="button" data-wizard-prev>Previous</button> <button type="button" data-wizard-next>Next</button> <button type="submit">Submit</button> </nav> </form>

Conditional Steps

Use data-wizard-if to show a step only when a form field matches a value. The step is hidden when the condition is not met and the progress bar adjusts automatically.

<form data-wizard> <fieldset data-wizard-step> <legend>Account Type</legend> <label> <input type="radio" name="accountType" value="personal" checked> Personal Account </label> <label> <input type="radio" name="accountType" value="business"> Business Account </label> </fieldset> <!-- Only shown when "business" is selected --> <fieldset data-wizard-step data-wizard-if="accountType:business"> <legend>Business Information</legend> <form-field> <label for="company">Company Name</label> <input type="text" id="company" name="company" required> </form-field> </fieldset> <fieldset data-wizard-step> <legend>Contact Information</legend> <!-- always shown --> </fieldset> <nav data-wizard-nav> <button type="button" data-wizard-prev>Previous</button> <button type="button" data-wizard-next>Next</button> <button type="submit">Submit</button> </nav> </form>

Optional Steps

Use data-wizard-optional to mark a step that can be skipped. The "Next" button advances without requiring validation on optional steps.

<form data-wizard> <fieldset data-wizard-step> <legend>Required Info</legend> <form-field> <label for="name">Name</label> <input type="text" id="name" name="name" required> </form-field> </fieldset> <!-- Can be skipped without validation --> <fieldset data-wizard-step data-wizard-optional> <legend>Additional Details (Optional)</legend> <form-field> <label for="bio">Bio</label> <textarea id="bio" name="bio" rows="3"></textarea> </form-field> </fieldset> <fieldset data-wizard-step> <legend>Done</legend> <p>Click Submit to finish.</p> </fieldset> <nav data-wizard-nav> <button type="button" data-wizard-prev>Previous</button> <button type="button" data-wizard-next>Next</button> <button type="submit">Submit</button> </nav> </form>

nav.steps Integration

When a nav.steps element is found inside the form (or referenced via data-wizard-steps="#id"), wizard.js automatically syncs step states — setting data-completed on past steps, aria-current="step" on the current step, and hiding <li> elements for conditional steps that don't apply.

<form data-wizard data-wizard-steps="#checkout-steps"> <nav class="steps" id="checkout-steps" aria-label="Checkout progress"> <ol> <li>Shipping</li> <li>Payment</li> <li>Review</li> </ol> </nav> <progress data-wizard-progress max="3" value="1"></progress> <fieldset data-wizard-step> <legend>Shipping</legend> <!-- shipping fields --> </fieldset> <fieldset data-wizard-step> <legend>Payment</legend> <!-- payment fields --> </fieldset> <fieldset data-wizard-step> <legend>Review</legend> <!-- review content --> </fieldset> <nav data-wizard-nav> <button type="button" data-wizard-prev>Previous</button> <button type="button" data-wizard-next>Next</button> <button type="submit">Place Order</button> </nav> </form>

Markup Reference

Attribute Element Purpose
data-wizard <form> Enables the wizard controller on the form
data-wizard-step <fieldset> Marks a fieldset as a wizard step
data-wizard-progress <progress> Auto-updated progress bar
data-wizard-nav <nav> Navigation container (shows/hides prev/next/submit buttons)
data-wizard-prev <button> Go to previous step
data-wizard-next <button> Go to next step (validates current step first)
data-wizard-if="..." <fieldset> Conditional step — shown only when condition is met
data-wizard-optional <fieldset> Step can be skipped without validation
data-wizard-steps="#id" <form> Points to a nav.steps element for auto-sync

Condition Syntax

The data-wizard-if attribute supports these patterns:

Pattern Meaning
data-wizard-if="fieldName:value" Show when field equals value
data-wizard-if="fieldName:!value" Show when field does NOT equal value
data-wizard-if="fieldName" Show when field is truthy (has value / is checked)
data-wizard-if="!fieldName" Show when field is falsy (empty / unchecked)

API & Events

Access the wizard programmatically via methods attached to the form element:

// Get the form const form = document.querySelector('#my-wizard'); // Navigation methods form.wizardNext(); // Go to next step (validates first) form.wizardPrev(); // Go to previous step form.wizardGoTo(2); // Jump to step index 2 (zero-based) form.wizardReset(); // Reset to first step // Access the controller const ctrl = form.wizardController; console.log(ctrl.currentStep); // Current step index console.log(ctrl.totalSteps); // Total visible steps // Listen for events form.addEventListener('wizard:stepchange', (e) => { console.log(`Moved from step ${e.detail.from} to ${e.detail.to}`); }); form.addEventListener('wizard:complete', () => { console.log('Wizard completed!'); });

Custom Events

Event Detail Fires when
wizard:stepchange { from, to } User navigates to a different step
wizard:complete none Form is submitted on the last step
wizard:reset none Wizard is reset to the first step

Configuration

Key configuration options for wizard patterns:

Property Purpose Values
data-size="sm|lg" Size of step indicator circles sm (1.5rem), default (2rem), lg (2.5rem)
data-labels="below" Position labels below circles On nav.steps
--progress-height Height of progress bar track 0.5rem, 0.75rem, 1rem
--progress-fill Progress bar fill color var(--color-interactive), var(--color-success)
aria-current="step" Marks current step for screen readers Apply to active step element
aria-valuenow Current progress percentage 0 to 100

Usage Notes

Step Validation

  • Validate each step before allowing progression to the next
  • Show inline validation errors using <output class="error">
  • Disable the "Next" button until required fields are valid
  • Consider allowing users to skip optional steps

Saving Progress

  • Auto-save form data to prevent loss on accidental navigation
  • Use sessionStorage or server-side persistence
  • Show a "saved" indicator when data is preserved
  • Allow users to resume incomplete forms

Accessibility

  • Use aria-current="step" on the active step for screen readers
  • Add role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax
  • Completed steps should have descriptive aria-label (e.g., "Go back to Account step (completed)")
  • Announce step changes to screen readers with live regions
  • Use aria-disabled="true" on future steps that cannot be accessed
  • Ensure keyboard navigation works for clickable completed steps

Navigation Best Practices

  • Always allow users to go back to previous steps
  • Preserve form data when navigating between steps
  • Disable future steps until prerequisites are complete
  • Show a summary/review step before final submission
  • Provide a "Save and exit" option for long forms

Related

Steps

Step indicator CSS pattern with variants and customization

Registration

Multi-step registration forms

Contact

Contact and inquiry forms

Form Field

Form field element with validation

Icon

Icon component for step indicators