HTML Formatterhtmlformatteronline.com

Multi-step form

A three-step form with a progress indicator, per-step validation and a review screen.

formmulti-stepwizardprogresscheckout
<form class="wizard" id="wizard" novalidate>

  <ol class="steps" id="steps">
    <li class="on" aria-current="step"><span>1</span>Details</li>
    <li><span>2</span>Delivery</li>
    <li><span>3</span>Review</li>
  </ol>

  <div class="bar"><i id="bar" style="width:33.3%"></i></div>

  <!-- step 1 -->
  <section class="step on" data-step="1" aria-label="Step 1 of 3, your details">
    <h2>Your details</h2>
    <div class="field">
      <label for="fname">Full name</label>
      <input type="text" id="fname" name="fname" autocomplete="name" required>
      <p class="err" hidden>Please enter your name.</p>
    </div>
    <div class="field">
      <label for="mail">Email address</label>
      <input type="email" id="mail" name="mail" autocomplete="email" required>
      <p class="err" hidden>Please enter a valid email address.</p>
    </div>
  </section>

  <!-- step 2 -->
  <section class="step" data-step="2" hidden aria-label="Step 2 of 3, delivery">
    <h2>Delivery</h2>
    <div class="field">
      <label for="addr">Street address</label>
      <input type="text" id="addr" name="addr" autocomplete="street-address" required>
      <p class="err" hidden>Please enter an address.</p>
    </div>
    <div class="two">
      <div class="field">
        <label for="city">City</label>
        <input type="text" id="city" name="city" autocomplete="address-level2" required>
        <p class="err" hidden>Please enter a city.</p>
      </div>
      <div class="field">
        <label for="post">Postcode</label>
        <input type="text" id="post" name="post" autocomplete="postal-code" required>
        <p class="err" hidden>Please enter a postcode.</p>
      </div>
    </div>
  </section>

  <!-- step 3 -->
  <section class="step" data-step="3" hidden aria-label="Step 3 of 3, review">
    <h2>Check and confirm</h2>
    <dl class="review" id="review"></dl>
    <label class="check">
      <input type="checkbox" id="terms" required>
      <span>I agree to the terms of sale.</span>
    </label>
    <p class="err" id="terms-err" hidden>Please agree to the terms before confirming.</p>
  </section>

  <div class="nav">
    <button type="button" class="ghost" id="back" hidden>Back</button>
    <button type="button" class="primary" id="next">Continue</button>
    <button type="submit" class="primary" id="done" hidden>Place order</button>
  </div>

  <p class="status" id="status" role="status"></p>
</form>

How it works

One form element, several sections. Not three separate forms. That keeps all the values in one place, lets the browser autofill across steps, and means the final submit carries everything without any state juggling.

Validate per step, not at the end. Nobody should reach step three and discover a problem on step one. Each Continue checks only its own step, and focus moves to the first field that failed.

Focus moves to the new step's heading. Without this, activating Continue leaves focus on a button that has just been replaced by different content, and a screen reader user hears nothing. The heading takes tabindex="-1" so it can be focused programmatically without joining the tab order.

The review step is not decoration. Showing people what they typed before they commit reduces support tickets more than any amount of inline validation. It costs one loop over the values.

The progress bar and the step list say the same thing twice — deliberately. The bar is a quick visual read; the numbered list names each stage so people know what is still to come.

Accessibility notes

Each step is a <section> with an aria-label naming its position: "Step 2 of 3, delivery". Hidden steps use the hidden attribute, which removes them from the accessibility tree entirely rather than merely hiding them visually.

aria-current="step" marks the active item in the progress list, and moves with it.

The review list is a real <dl>, so labels and values are programmatically paired rather than being two columns that only look related.

Making it yours

Three steps is about the limit before a single well-organised page is better. The evidence on multi-step forms is mixed: they raise completion when the alternative is an intimidating wall of forty fields, and lower it when they split six fields into three screens.

To persist progress across a reload, write the values to sessionStorage on each step change and restore them on load. Never put payment details anywhere near browser storage.

The autocomplete tokens here — street-address, address-level2, postal-code — are the standard ones. Browsers fill a whole address in one tap when you use them, which matters more on mobile than anything else on this page.

Related templates

Check your version

Once you have edited this, the HTML validator will catch any tag you left unclosed, and the formatter will tidy the indentation. Both run in your browser.