⚡IntermediateAdvanced Forms
Multi-Step Form Structure
Build accessible multi-step forms using <fieldset>, aria-current, and progress indicators.
Overview
Multi-step forms (wizards) break a long form into sequential steps to reduce cognitive load. The key HTML considerations are:
1. Structure: each step is a <fieldset> with a <legend>, or a separate section revealed progressively
2. Progress: use <nav> with numbered steps or a <progress> element
3. Accessibility: use aria-current="step" on the active step, aria-label on the nav, and announce step changes via aria-live
4. Navigation: "Previous" buttons should not submit; use type="button"
5. Data preservation: use hidden inputs to carry forward values from previous steps, or handle in JS
Key Points
- •Each step is typically a <fieldset> with a <legend> describing that step
- •Show one step at a time with hidden attribute or CSS display:none
- •aria-current="step" marks the active step in a step navigation
- •aria-label on the step nav describes its purpose to screen readers
- •Previous/Next buttons must be type="button" to avoid form submission
- •<progress value="step" max="total"> visually communicates how far along the user is
Examples
Multi-step form HTML structure
<form id="signup-wizard" novalidate>
<!-- Step navigation -->
<nav aria-label="Form steps">
<ol>
<li aria-current="step">1. Account</li>
<li>2. Profile</li>
<li>3. Review</li>
</ol>
</nav>
<progress id="wizard-progress" value="1" max="3"></progress>
<!-- Step 1 -->
<fieldset id="step-1">
<legend>Account Details</legend>
<label for="email">Email</label>
<input type="email" id="email" name="email" required autocomplete="email">
<label for="password">Password</label>
<input type="password" id="password" name="password" required minlength="8" autocomplete="new-password">
</fieldset>
<!-- Step 2 (hidden initially) -->
<fieldset id="step-2" hidden>
<legend>Your Profile</legend>
<label for="fname">First Name</label>
<input type="text" id="fname" name="fname" required autocomplete="given-name">
<label for="lname">Last Name</label>
<input type="text" id="lname" name="lname" required autocomplete="family-name">
</fieldset>
<!-- Step 3 (hidden initially) -->
<fieldset id="step-3" hidden>
<legend>Review & Submit</legend>
<p>Review your information before submitting.</p>
</fieldset>
<!-- Navigation buttons -->
<div class="form-nav">
<button type="button" id="prev-btn" hidden>Previous</button>
<button type="button" id="next-btn">Next</button>
<button type="submit" id="submit-btn" hidden>Create Account</button>
</div>
</form>JavaScript step controller
const steps = document.querySelectorAll('fieldset[id^="step-"]');
const navItems = document.querySelectorAll('nav li');
const progress = document.getElementById('wizard-progress');
let current = 0;
function showStep(index) {
steps.forEach((s, i) => s.hidden = i !== index);
navItems.forEach((li, i) => {
li.removeAttribute('aria-current');
if (i === index) li.setAttribute('aria-current', 'step');
});
progress.value = index + 1;
document.getElementById('prev-btn').hidden = index === 0;
document.getElementById('next-btn').hidden = index === steps.length - 1;
document.getElementById('submit-btn').hidden = index !== steps.length - 1;
}
document.getElementById('next-btn').addEventListener('click', () => {
// Validate current step before advancing
const inputs = steps[current].querySelectorAll('input, select, textarea');
const valid = [...inputs].every(i => i.checkValidity());
if (valid) showStep(++current);
else inputs.forEach(i => !i.checkValidity() && i.reportValidity());
});
document.getElementById('prev-btn').addEventListener('click', () => {
showStep(--current);
});Common Mistakes
- Using type="submit" on Next buttons accidentally submits the form before the last step
- Not validating each step before advancing invalid data carries forward silently
- Forgetting aria-current="step" on the active step screen readers can't tell where the user is
Note
Put all steps in a single <form> so the browser's built-in password manager and autofill work across steps. Use hidden to hide inactive fieldsets rather than removing them from the DOM their values are preserved.
Exercises
Write HTML to solve each exercise. Use Preview to see it render in a live iframe, then mark as done.
Loading editor…
Completed: 0 / 5 exercises
Knowledge Check
Question 1 of 4