fieldset per step, aria-current in the stepper, and loss free back navigation in the wizard pattern
A multi-step form is more than a sequence of individual form pages: it needs a recognizable grouping per step, a progress indicator that clearly communicates the current step, an active announcement of every step change for screen reader users, and back navigation that reliably preserves entered data. This general wizard pattern applies to registration wizards, multi-step configurators and surveys just as much as to any other multi-step form process outside of checkout.
Table of Contents
- 1. The wizard pattern as its own, general problem
- 2. fieldset and legend: grouping form fields per step
- 3. Progress stepper markup: aria-current for the active step
- 4. Screen reader announcement of step changes
- 5. Focus management on step change: moving focus to the step heading
- 6. Per step validation before advancing and an error summary
- 7. Interim saving and back navigation without data loss
- 8. Clickable versus display only steps: aria-disabled for unreached steps
- 9. Implementation in Magento and Hyvä: a generic Alpine.js wizard component
- 10. Summary
- 11. FAQ
1. The wizard pattern as its own, general problem
Multi-step forms show up in very different contexts: registration wizards with multiple profile steps, product configurators with steps building on each other, multi page surveys, or support forms with conditional branching. All of these cases share the same technical base structure independent of any specific checkout context, which is why the wizard pattern deserves treatment as its own, reusable accessibility topic.
Four building blocks keep recurring here and must each be implemented correctly independent of one another: the semantic grouping of fields within a step, the visual and semantic progress indicator across all steps, the active announcement of a step change, and the loss free handling of already entered data during forward and backward navigation.
2. fieldset and legend: grouping form fields per step
Every individual step of a multi-step form should be marked up as its own fieldset element, with a legend that briefly summarizes the step's purpose, for example "Step 2 of 4: Contact details". This grouping is more than pure cosmetics, it makes sure a screen reader automatically announces the context of the entire step when the first field in that step gets focused, not just the individual field label.
Without fieldset/legend, a user has to piece together the current step's context solely from individual field labels, which quickly leads to confusion, especially with similarly named fields across different steps, for example multiple address fields in different contexts.
<fieldset class="wizard-step" data-step="2">
<legend>Step 2 of 4: Contact details</legend>
<label for="email">Email address</label>
<input type="email" id="email" name="email" required>
<label for="phone">Phone number</label>
<input type="tel" id="phone" name="phone">
</fieldset>
3. Progress stepper markup: aria-current for the active step
The progress indicator itself is usually implemented as a nav element containing an ordered list ol, where every li represents one step. The currently active step gets the attribute aria-current="step", a special value of aria-current meant exactly for this use case, distinct from the more generic values page or true, and it tells screen readers explicitly that this represents a step in a sequential process.
Already completed steps should be visually and semantically distinguishable from steps not yet reached, for example via an additional, hidden text like "completed" or "not yet reached" inside every list item. This status must never be communicated exclusively through a checkmark graphic or a color change, since both stay invisible to screen readers without accompanying text.
<nav aria-label="Progress">
<ol class="stepper">
<li>
<span class="sr-only">Completed: </span>Account
</li>
<li aria-current="step">
<span class="sr-only">Current step: </span>Contact details
</li>
<li>
<span class="sr-only">Not yet reached: </span>Address
</li>
<li>
<span class="sr-only">Not yet reached: </span>Confirmation
</li>
</ol>
</nav>
4. Screen reader announcement of step changes
When a user moves from one step to the next via a next button, a typical single page wizard implementation only changes the visible content in the DOM, without a full page navigation happening. Without an active announcement, a screen reader user may not notice this change at all, since focus stays at the same spot where the next button used to be.
An aria-live="polite" region that outputs a short text like "Step 3 of 4: Address" on every step change makes the transition reliably audible. This announcement does not replace the focus management from the next section, it complements it, since a live region still works even when focus cannot be set exactly right for technical reasons at a given moment.
<div id="step-announcement" class="sr-only" aria-live="polite"></div>
<script>
function goToStep(stepNumber, stepLabel, totalSteps) {
document.getElementById('step-announcement').textContent =
'Step ' + stepNumber + ' of ' + totalSteps + ': ' + stepLabel;
}
</script>
5. Focus management on step change: moving focus to the step heading
In addition to the live region announcement, keyboard focus should be actively moved to the heading or the legend of the new step on every step change, usually via tabindex="-1" plus .focus() in JavaScript, since non interactive elements like headings are not focusable by default. This step ensures keyboard users can continue working right at the start of the new step's content after the change, instead of having to navigate there manually.
If focus instead stays on the next button of the previous step, which no longer exists in the new step or sits somewhere else, the user loses orientation and has to tab through the entire new form content just to figure out what changed.
function focusStepHeading(stepEl) {
const heading = stepEl.querySelector('legend, h2');
if (heading) {
heading.setAttribute('tabindex', '-1');
heading.focus();
}
}
6. Per step validation before advancing and an error summary
Clicking next should first fully validate the current step before moving to the next one. If errors occur, the step change must not happen, and focus must be moved to an error summary at the top of the current step, listing every invalid field with a jump link to the respective field.
This error summary follows the same pattern used for single step form validation, but here it applies repeatedly per step, since every step is effectively its own validation section. It matters that the error message clearly indicates which step the error is in, especially if a later summary page collects errors across all steps.
7. Interim saving and back navigation without data loss
When a user navigates back to a previous step via a back button or a clickable step in the stepper, already entered values must still be present there. Technically this can be solved either via client side state, for example an Alpine store or sessionStorage, or via server side interim saving on every step change, with the latter additionally protecting against data loss on an accidental browser reload.
From an accessibility standpoint, what matters is that back navigation never triggers a complete restart of the form, but returns exactly to the previous state including all entered values, with focus correctly set on the step heading and aria-current correctly updated in the stepper. A user who accidentally goes back a step must not be punished by having to retype data already entered.
document.addEventListener('alpine:init', () => {
Alpine.data('wizard', () => ({
currentStep: 1,
values: Alpine.$persist({}).as('wizard-values'),
goTo(step) {
this.currentStep = step;
this.$nextTick(() => focusStepHeading(this.$refs['step-' + step]));
},
}));
});
8. Clickable versus display only steps: aria-disabled for unreached steps
Whether users are allowed to jump directly to a later, not yet reached step in the stepper depends on the specific use case. If a direct jump does not make sense, because the later step depends on data from an earlier, not yet validated step, the corresponding stepper entry should be marked with aria-disabled="true", rather than simply graying it out visually without communicating that semantically.
Already completed, earlier steps, on the other hand, should almost always stay clickable, since users frequently want to jump back to correct an entry. A clickable, already completed step entry should be implemented as a button or a element, not as a plain li with a click handler and no native interactive semantics.
9. Implementation in Magento and Hyvä: a generic Alpine.js wizard component
Since the wizard pattern is not limited to checkout, it is worth building a generic Alpine.js component that encapsulates step state, per step validation, focus management and the aria-live announcement, and can be reused for a registration wizard just as well as for a product configurator or a multi-step contact form. The concrete form fields per step stay swappable, while the accessibility logic is maintained centrally in one place.
Every inline script block for focus management or step change announcement must be registered via $hyvaCsp->registerInlineScript() in the corresponding phtml template, so the Content Security Policy does not block execution.
<div x-data="wizard" x-cloak>
<nav aria-label="Progress">
<ol class="stepper">
<template x-for="(step, index) in steps" :key="step.id">
<li :aria-current="currentStep === index + 1 ? 'step' : null">
<span x-text="step.label"></span>
</li>
</template>
</ol>
</nav>
<div id="step-announcement" class="sr-only" aria-live="polite" x-text="announcement"></div>
</div>
| Building block | Technique | Purpose | Common mistake |
|---|---|---|---|
| Field grouping | fieldset plus legend per step | Automatically announcing context on focus | Only individual field labels with no group context |
| Progress indicator | aria-current=step in the stepper | Clearly marking the current step | Only visual highlighting with no ARIA |
| Step change | aria-live region with a step announcement | Making the change audible without a page reload | Silent DOM change with no announcement |
| Focus after change | Focus on the new step's heading | Enabling users to continue right away | Focus stays on a now vanished button |
| Back navigation | State storage per step | Preserving entered values | Form restarts on going back |
Mironsoft
WCAG audits, accessible Magento shops, and training
Not sure whether the shop is actually accessible?
We audit existing Magento shops against WCAG 2.2, fix concrete barriers in the Hyvä frontend, and train teams so accessibility stays anchored in the development process for good.
WCAG Audit
Systematically review the shop against WCAG 2.2 AA, with a prioritized issue list.
Fixing Barriers
Concrete implementation: keyboard operability, screen reader support, contrast, forms.
Team Training
Raise developer and editor awareness for accessible implementation day to day.
10. Summary
Accessible Multi-Step Forms: The Essentials
Core idea
fieldset and legend per step automatically supply context as soon as a user focuses the first field of a step.
Progress indicator
aria-current=step semantically marks the current step unambiguously, in addition to any visual highlighting.
Step change
An aria-live announcement plus deliberate focus management on the new step's heading make every change traceable.
Data preservation
Back navigation must never discard already entered values, regardless of whether storage happens client side or server side.