Stepper
A form spread over several panels, one step at a time, with a header that shows where you are.
Linear with validation
The header is an <ol> of buttons. Each
<section> after it is the panel of the step in the same
position. With data-linear a user cannot jump ahead, and with
data-validate the fields of the current panel must pass before
Next moves on. The last Next fires tsunami:stepcomplete. The
last panel here is a review, filled from a tsunami:stepchange
listener.
Create your account
Choose a plan
Review
Step 1 of 3.
<div class="stepper" id="wiz" data-linear data-validate>
<ol>
<li><button type="button"><span><strong>Account</strong><small>Name and email</small></span></button></li>
<li><button type="button"><span><strong>Plan</strong><small>Pick one</small></span></button></li>
<li><button type="button"><span><strong>Review</strong><small>Check and finish</small></span></button></li>
</ol>
<section>
<h3>Create your account</h3>
<div class="stack" data-gap="sm">
<div class="field">
<label for="wz-name">Name *</label>
<input id="wz-name" name="wz-name" type="text" required autocomplete="name">
</div>
<div class="field">
<label for="wz-email">Email *</label>
<input id="wz-email" name="wz-email" type="email" required autocomplete="email">
<small id="wz-email-h">We send the receipt here.</small>
</div>
</div>
<footer><button type="button" class="btn" data-variant="primary" data-next>Next</button></footer>
</section>
<section>
<h3>Choose a plan</h3>
<div class="field">
<label for="wz-plan">Plan *</label>
<select id="wz-plan" name="plan" required>
<option value="">Select a plan</option>
<option>Starter</option>
<option>Team</option>
</select>
</div>
<footer><button type="button" class="btn" data-prev>Back</button><button type="button" class="btn" data-variant="primary" data-next>Next</button></footer>
</section>
<section>
<h3>Review</h3>
<dl id="wz-review"></dl>
<footer><button type="button" class="btn" data-prev>Back</button><button type="button" class="btn" data-variant="primary" data-next>Finish</button></footer>
</section>
<p id="wz-log" role="status">Step 1 of 3.</p>
</div>
<script type="module">
const wiz = document.getElementById("wiz");
const log = document.getElementById("wz-log");
wiz.addEventListener("tsunami:stepchange", (e) => {
log.textContent = `Step ${e.detail.index + 1} of 3.`;
if (e.detail.index !== 2) return;
const review = document.getElementById("wz-review");
review.replaceChildren();
for (const id of ["wz-name", "wz-email", "wz-plan"]) {
const dt = document.createElement("dt");
dt.textContent = document.querySelector(`label[for="${id}"]`).textContent;
const dd = document.createElement("dd");
dd.textContent = document.getElementById(id).value;
review.append(dt, dd);
}
});
wiz.addEventListener("tsunami:stepcomplete", () => {
log.textContent = "Done. Nothing was sent: this is a demo.";
});
</script>
Vertical, with an optional step
data-orientation="vertical" stacks the steps (under 40rem they
always stack). A panel with data-optional is not validated, so
the user can move past it empty. Without data-linear the header
steps are free to click, but going forward still validates the current
panel.
Company
Branding
Billing
<div class="stepper" data-orientation="vertical" data-validate>
<ol>
<li><button type="button"><span><strong>Company</strong><small>Required</small></span></button></li>
<li><button type="button"><span><strong>Branding</strong><small>Optional</small></span></button></li>
<li><button type="button"><span><strong>Billing</strong></span></button></li>
</ol>
<section>
<h3>Company</h3>
<div class="field">
<label for="vs-co">Company name *</label>
<input id="vs-co" name="vs-co" type="text" required>
</div>
<footer><button type="button" class="btn" data-variant="primary" data-next>Next</button></footer>
</section>
<section data-optional>
<h3>Branding</h3>
<div class="field">
<label for="vs-site">Website</label>
<input id="vs-site" name="vs-site" type="url" >
<small id="vs-site-h">Skip this step if you like.</small>
</div>
<footer><button type="button" class="btn" data-prev>Back</button><button type="button" class="btn" data-variant="primary" data-next>Next</button></footer>
</section>
<section>
<h3>Billing</h3>
<div class="field">
<label for="vs-vat">VAT number *</label>
<input id="vs-vat" name="vs-vat" type="text" required>
</div>
<footer><button type="button" class="btn" data-prev>Back</button><button type="button" class="btn" data-variant="primary" data-next>Finish</button></footer>
</section>
</div>
Free navigation and the API
With neither data-linear nor data-validate any
step can be opened from the header. Move it from script with
tsunami.stepper.
Step one
Free navigation: click any step.
Step two
Nothing is validated here.
Step three
The last one.
<div class="stepper" id="api-demo">
<ol>
<li><button type="button"><span><strong>One</strong></span></button></li>
<li><button type="button"><span><strong>Two</strong></span></button></li>
<li><button type="button"><span><strong>Three</strong></span></button></li>
</ol>
<section><h3>Step one</h3><p>Free navigation: click any step.</p></section>
<section><h3>Step two</h3><p>Nothing is validated here.</p></section>
<section><h3>Step three</h3><p>The last one.</p></section>
</div>
<div class="stack" data-direction="row" data-gap="sm">
<button type="button" class="btn" onclick="tsunami.stepper.prev('#api-demo')">prev</button>
<button type="button" class="btn" onclick="tsunami.stepper.next('#api-demo')">next</button>
<button type="button" class="btn" onclick="tsunami.stepper.go('#api-demo', 2)">go(2)</button>
</div>
Behavior
JS sets data-state (done, current or
upcoming) on each step button from the current step, and
aria-current="step" on the current one. It shows only the
current panel and gives each panel role="group", labelled by
its step. The first step is current unless the markup marks another with
data-state="current". Moving with a button or the API moves
focus to the panel heading, or to its first field when there is no heading.
Going back never validates. Pressing Enter in a field works as Next, unless
the panel has a submit button.
API
Lives in js/stepper.js. It uses event delegation, so steppers
added later work too. Each function takes the stepper or any element inside
it, or a selector.
Methods
| Function | Arguments | Description |
|---|---|---|
tsunami.stepper.go(el, index) | index: number from 0 | Opens a step. Going forward validates first (every panel on the way when linear). Returns false if blocked or out of range. |
tsunami.stepper.next(el) | Next step. On the last step it validates and fires tsunami:stepcomplete. | |
tsunami.stepper.prev(el) | Previous step. Never validates. | |
tsunami.stepper.index(el) | The current step number, from 0. |
Events
Both fire on the stepper and bubble.
| Event | Detail | Description |
|---|---|---|
tsunami:stepchange | { index, step, panel } | After a step opens. step is its header <li>. |
tsunami:stepcomplete | { index } | Next on the last step, once its fields are valid. |
Keyboard
| Key | Context | Description |
|---|---|---|
Tab / Shift+Tab | header | Steps are buttons. Disabled steps are skipped. |
Arrow keys | header | Move focus between enabled steps, wrapping. They do not open one. |
Home / End | header | Focus the first / last enabled step. |
Enter / Space | header | Open the focused step. |
Enter | field in a panel | Same as Next, unless the panel has a submit button. |
Attributes
| Attribute / element | Values | Description |
|---|---|---|
.stepper | div | Wraps the <ol> and the panels |
> ol > li > button | type="button" | A step. Holds a span with strong (label) and an optional small (description). |
> section | one per step, in order | A panel. Put a heading first and a footer with the buttons. |
data-state | done · current · upcoming | On a step button. Set by JS. In markup, current picks the first step. |
data-linear | present / absent | On the stepper: steps after the current one are disabled (aria-disabled). |
data-validate | present / absent | On the stepper: Next and forward jumps run reportValidity() on the current panel's fields. |
data-optional | present / absent | On a panel: not validated. |
data-orientation | vertical | On the stepper: stack the steps. Under 40rem it is always vertical. |
data-next · data-prev | on a button in a panel | Next (or finish on the last panel) and Back. |