FORM WIZARD: Multi-step form a parent can complete without losing work or submitting twice
COMPOSES: .steps + steps_controller.js | .popup + popup_controller.js (discard-confirm) | .inline-notification-error (error summary) | .btn-loading + .spinner-border (submit guard) | form suite

This is one fully interactive instance — try each behaviour directly. Back preserves data: fill in Step 1, click Continue, then Back — your answers are still there (panels are never destroyed, only hidden). Validation-blocked advance: leave a required field empty and click Continue — an error summary appears above the step title, focus moves to it, and the failing step gets a red exclamation mark in the rail. Discard-confirm: type something, then click Cancel — a popup asks you to confirm; Escape/overlay-click/"Keep editing" all resolve to keeping your answers, only "Discard" clears them. Review as page content: Step 3 reads back everything you entered with "Change" links back to the relevant step. This instance also persists to sessionStorage (persist-key-value="apply-demo") — refresh the page after filling a field and your answers (and current step) come back.

Live demo — Apply to Hills Road Sixth Form College

Page-level only: this pattern must never be hosted inside .popuppopup_controller.close() isn't cancelable and Escape closes the topmost popup immediately, which would destroy the wizard's data before a discard-confirm could open. The discard-confirm itself is the one place this demo composes .popup.

1
Child's details
2
School preferences
3
Review & submit

Child's details

As it appears on their birth certificate.

School preferences

Review your application

Check your answers before submitting. Nothing is sent until you press "Submit application".

Child's details

Change
First name
Not provided
Last name
Not provided
Date of birth
Not provided
Current school
Not provided

School preferences

Change
First-choice school
Not selected
Parent email
Not provided
Sibling attends
No
<div class="form-wizard steps" data-controller="steps form-wizard" data-form-wizard-persist-key-value="apply-demo" data-action="steps:beforeNext->form-wizard#handleBeforeNext steps:changed->form-wizard#handleStepsChanged steps:completed->form-wizard#handleCompleted submit->form-wizard#handleSubmit"><form novalidate data-form-wizard-target="form"><div class="form-wizard-steps steps-header">…</div><p class="form-wizard-progress" data-form-wizard-target="progress"></p><div class="steps-content"><div class="form-wizard-panel steps-panel active" data-steps-target="panel" data-form-wizard-target="panel"><h2 class="form-wizard-panel-title" tabindex="-1" data-form-wizard-target="panelTitle">…</h2>…</div>…</div><div class="form-wizard-footer steps-footer"><div class="form-wizard-footer-exit"><a class="btn btn-link" data-form-wizard-target="discardTrigger" data-action="click->form-wizard#requestDiscard">Cancel</a></div><button type="button" data-steps-target="backBtn" data-action="click->steps#back">Back</button><button type="button" data-form-wizard-target="submitBtn" data-action="click->steps#next">Continue</button></div></form><div data-form-wizard-role="discard-popup" data-controller="popup">…</div></div>

Error summary + field marking (C08 recipe)

Timing: validation only runs on Continue/Submit — never on keystroke. Once a field is marked invalid, it re-validates live on input/blur and clears itself (and its summary row) the instant it passes. Each summary link's text is the identical string as that field's .invalid-feedback — both are set from the same data-form-wizard-error source, so the two can never drift apart.

Enter your child's first name

As it appears on their birth certificate.

Enter your child's date of birth
<div class="form-wizard-error-summary inline-notification inline-notification-error" role="alert" tabindex="-1"><div class="inline-notification-icon">…</div><div class="inline-notification-content"><p class="inline-notification-title">1 answer needs attention before you continue</p><div class="inline-notification-message"><ul class="form-wizard-error-summary-list"><li><a href="#field-id" class="form-wizard-error-summary-link">Enter your child's first name</a></li></ul></div></div></div> … <div class="invalid-feedback" id="field-id-error">Enter your child's first name</div>