Welcome to Vanilla Breeze
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
This bell pulls live notifications from /go/notify/messages — the same contract documented at /docs/concepts/service-contracts/. Static articles like this one are the no-JS / no-backend fallback.
Multi-step form patterns with numbered steps, tab navigation, and progress bar indicators. Guide users through complex forms with clear progress feedback.
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 progressnav.steps integration — auto-syncs step indicator state; auto-populates from legends when emptydata-wizard-if) — show/hide steps based on user input, with AND/OR compound expressionsdata-wizard-optional) — skip without validationdata-wizard-summary) — auto-populate a review step with field valueswizardNext(), wizardPrev(), wizardGoTo(), wizardReset()wizard:step-change, wizard:complete, wizard:resetaria-current="step", live regions, roving tabindex keyboard navigationA 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>
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. */
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>
.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;}
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>
.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;}
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).
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>
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>
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>
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>
| 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) |
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 (||).
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.
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>
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>
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>
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>
The step indicator (nav.steps) uses the roving tabindex pattern for keyboard access:
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.
Access the wizard programmatically via methods attached to the form element:
| 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 |
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 |
<output class="error">sessionStorage or server-side persistencearia-current="step" on the active step for screen readersrole="progressbar" with aria-valuenow, aria-valuemin, aria-valuemaxaria-label (e.g., "Go back to Account step (completed)")aria-disabled="true" on future steps that cannot be accessedStep indicator CSS pattern with variants and customization
Multi-step registration forms
Contact and inquiry forms
Form field element with validation
Icon component for step indicators