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.jscontroller — auto-initializes onform[data-wizard], handles step navigation, validation, and progressnav.stepsintegration — auto-syncs step indicator state; auto-populates from legends when empty- Conditional steps (
data-wizard-if) — show/hide steps based on user input, with AND/OR compound expressions - Optional steps (
data-wizard-optional) — skip without validation - Summary/review step (
data-wizard-summary) — auto-populate a review step with field values - Auto-injected navigation — Back/Next/Submit buttons created automatically when nav is omitted
- Programmatic API —
wizardNext(),wizardPrev(),wizardGoTo(),wizardReset() - Custom events —
wizard:step-change,wizard:complete,wizard:reset - Visual patterns — numbered step indicators, tab navigation, and progress bars
- Accessibility —
aria-current="step", live regions, roving tabindex 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(--size-xs); padding: var(--size-m) var(--size-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(--font-size-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(--size-s);}.progress-step { font-weight: 600; color: var(--color-text);}.progress-label { font-size: var(--font-size-sm); 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(--font-size-xs); color: var(--color-text-muted); margin-top: var(--size-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-summary |
<fieldset> |
Summary/review step — auto-populated with field values |
data-wizard-field="name" |
any element | Inside summary step, displays the named field's value |
data-wizard-steps="#id" |
<form> |
Points to a nav.steps element for auto-sync (auto-populates from legends if empty) |
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) |
data-wizard-if="a:x && b:y" |
AND — show when all conditions are true |
data-wizard-if="a:x || a:y" |
OR — show when any condition is true |
AND (&&) has higher precedence than OR (||).
Summary / Review Step
Add data-wizard-summary to a step fieldset to create a review step. The wizard populates it with form values each time the step becomes active.
Manual Mode
Place elements with data-wizard-field="fieldName" to control exactly which values appear and where:
<fieldset data-wizard-step data-wizard-summary> <legend>Review</legend> <dl> <dt>Email</dt> <dd data-wizard-field="email"></dd> <dt>Name</dt> <dd data-wizard-field="fullname"></dd> </dl></fieldset>
Auto Mode
If no data-wizard-field elements are present, a <dl> is auto-generated from all non-empty fields across visible steps. Labels are detected from <label> associations.
<fieldset data-wizard-step data-wizard-summary> <legend>Review</legend> <!-- dl auto-generated from all form values --></fieldset>
Auto-Injected Navigation
If no [data-wizard-nav] element exists in the form, the wizard automatically injects a Back / Next / Submit navigation bar. This allows minimal wizard markup:
<form data-wizard> <fieldset data-wizard-step><legend>Step 1</legend>...</fieldset> <fieldset data-wizard-step><legend>Step 2</legend>...</fieldset> <!-- nav injected automatically --></form>
Auto-Populated Step List
When a nav.steps element contains an empty <ol>, the wizard auto-populates <li> items from each step's <legend> text. This eliminates the need to duplicate step names between fieldset legends and the navigation list.
<form data-wizard> <nav class="steps" aria-label="Progress"> <ol><!-- auto-populated from legends --></ol> </nav> <fieldset data-wizard-step><legend>Account</legend>...</fieldset> <fieldset data-wizard-step><legend>Profile</legend>...</fieldset> <fieldset data-wizard-step><legend>Confirm</legend>...</fieldset></form>
Keyboard Navigation
The step indicator (nav.steps) uses the roving tabindex pattern for keyboard access:
- Arrow Right / Arrow Down — move focus to next step item
- Arrow Left / Arrow Up — move focus to previous step item
- Home — move focus to first step item
- End — move focus to last step item
- Enter / Space — activate completed step items to navigate back
Only the current step item is in the tab order (tabindex="0"). All other items have tabindex="-1" and are reachable only via arrow keys.
API & Events
Access the wizard programmatically via methods attached to the form element:
Custom Events
| Event | Detail | Fires when |
|---|---|---|
wizard:step-change |
{ 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
sessionStorageor 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"witharia-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