⚡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 &amp; 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

Why must Next/Previous buttons in a multi-step form use type="button"?