LEARNING OBJECTIVES ⌵
- Model multi-step checkout flows as a deterministic Finite State Machine (FSM).
- Implement strict step-level validation guards preventing forward navigation until prerequisites pass.
- Build accessible progress stepper indicators using
<nav>,<ol>, andaria-current="step". - Manage programmatic focus routing between step transitions (
tabindex="-1"on fieldsets/headings). - Consolidate distributed multi-step inputs into a single immutable Order Review payload.
📖 The Mental Model & Story (Intuitive Foundation)
Imagine a multi-stage space rocket launch. You cannot ignite the Stage 2 orbital thrusters while the Stage 1 booster clamps are still engaged. Every phase of the launch has a rigid checklist: Pre-Flight Telemetry (Step 1), Atmospheric Ascent (Step 2), Orbital Insertion (Step 3), and Payload Deployment (Step 4). If any single sensor in Stage 1 fails inspection, the launch sequencer halts immediately—it does not let you skip ahead to Stage 3.
An Enterprise Multi-Step Wizard is that launch sequencer. By breaking an intimidating 30-field form into bite-sized, thematic stages (Shipping ➔ Shipping Method ➔ Payment ➔ Review), cognitive load drops dramatically.
Your JavaScript acts as mission control: ensuring the user cannot advance without passing the active stage's validation gate, storing state in durable session memory, and presenting a transparent final review before the final payload is dispatched.
Technical Deep Dive & Specifications
The Multi-Step Finite State Machine (FSM)
[ Step 1: Shipping ] ──( Valid? )──> [ Step 2: Method ] ──( Valid? )──> [ Step 3: Payment ] ──( Valid? )──> [ Step 4: Review ]
▲ ▲ ▲
│ │ │
└──( Prev )──────────────────────────┴──( Prev )─────────────────────────┴──( Prev )
The "Hidden Invalid Input" Native Constraint Hazard
One of the most dangerous bugs in multi-step wizard engineering occurs when using native HTML5 required attributes on hidden steps:
THE BROWSER TRAP: If Step 3 contains
<input required>and is hidden withdisplay: none, clicking submit in Step 1 causes modern browsers to throw an unhandled console error:An invalid form control with name='cvv' is not focusable.The form submission silently freezes because the browser tries to focus the invalid input in hidden Step 3!
The Solution:
- Always mark the main
<form>withnovalidateto take manual control of the validation lifecycle. - Validate only the active step's
<fieldset>before advancing:
function validateActiveStep(stepIndex) {
const currentFieldset = stepContainers[stepIndex];
const inputs = currentFieldset.querySelectorAll('input, select, textarea');
let isValid = true;
inputs.forEach(input => {
if (!input.checkValidity()) {
isValid = false;
input.classList.add('invalid');
} else {
input.classList.remove('invalid');
}
});
return isValid;
}
Accessible Stepper Navigation Schema
The progress bar at the top of the wizard must inform screen readers of progress:
<nav aria-label="Checkout Progress">
<ol class="stepper-list">
<li class="step-item is-complete">
<span class="sr-only">Step 1: </span>Shipping Details (Completed)
</li>
<li class="step-item is-active" aria-current="step">
<span class="sr-only">Step 2: </span>Delivery Options (Current)
</li>
<li class="step-item is-upcoming">
<span class="sr-only">Step 3: </span>Payment & Review
</li>
</ol>
</nav>
Focus Routing Between Steps
When navigating from Step 1 to Step 2, keyboard and screen reader focus must not remain stuck on the bottom "Next" button. It must be programmatically moved to the newly revealed step's <legend> or heading:
function goToStep(nextIndex) {
// Hide current step, show next step
stepContainers[currentStep].hidden = true;
stepContainers[nextIndex].hidden = false;
currentStep = nextIndex;
updateStepperUI();
// Focus the new step heading for accessibility
const stepHeading = stepContainers[nextIndex].querySelector('legend, h3');
if (stepHeading) {
stepHeading.setAttribute('tabindex', '-1');
stepHeading.focus();
}
}
💻 Interactive Code Playground
Starter Code
Line-by-Line Code Breakdown
- Lines 129–147 (
<ol class="stepper">): Accessible ordered list tracking wizard progression witharia-current="step"applied dynamically to the active pill. - Lines 151, 168, 185, 199 (
<fieldset class="wizard-step" hidden>): Encapsulates each step in an individual<fieldset>with standard<legend>headings. - Lines 237–251 (
validateStep(index)): Scans only the inputs inside the currently visible<fieldset>, validating step-by-step without tripping over hidden future inputs. - Lines 253–280 (
updateUI()): Synchronizes step visibility (hiddenproperty), stepper pill state, button visibility, and shifts programmatic keyboard focus to the new<legend>. - Lines 282–289 (
populateReview()): Harvests aggregated data from all steps usingnew FormData(form)and formats a secure masked review table (e.g.•••• 9010).
Expected Browser Render Output
+-------------------------------------------------------------+
| (1) Shipping ─── (2) Delivery ─── (3) Payment ─── (4) Review|
| |
| Shipping Address |
| |
| Full Recipient Name |
| [ Jane Doe ] |
| |
| Street Address |
| [ 123 Market St, Suite 400 ] |
| |
| Zip / Postal Code |
| [ 94105 ] |
| |
| ----------------------------------------------------------- |
| [ Back ] [ Continue ➔ ] |
+-------------------------------------------------------------+🏋️ Hands-On Exercise
🎯 The Challenge: Add sessionStorage Draft Hydration
Instructions:
- Extend the multi-step checkout wizard so that whenever a user transitions between steps (clicking Next or Back), the current step index and form state are saved to
sessionStorage. - If the user refreshes the page on Step 3, the wizard should:
- Restore all previous inputs from Step 1, 2, and 3.
- Automatically reopen directly to Step 3.
- Update the stepper pills accordingly.
- Clear
sessionStorageupon final order submission.
🏁 Starter Code Sandbox
⚠️ Common Pitfalls
- Relying on Native Form Validation Across Hidden Steps: If a hidden step has an invalid
<input required>, callingform.checkValidity()will fail to submit and throw non-focusable control errors. Always validate steps individually. - Losing Keyboard Focus During Step Transitions: When Step 1 disappears, focus is dropped to the
<body>element. Always route focus to the incoming step's heading or<legend>usingtabindex="-1". - Failing to Mask Payment Data on the Review Step: Never print raw 16-digit credit card numbers or CVVs on the confirmation screen. Always truncate to the last 4 digits (
•••• 1234).
💡 Pro Tips
- Integrate with the History API: Push URL hashes or states (
history.pushState({ step: 2 }, '', '#step-2')) so the browser's native Back button navigates between wizard steps instead of ejecting the user from the site. - Track Completion Telemetry: Send analytics beacons (e.g.
navigator.sendBeacon()) on step drop-offs to pinpoint funnel friction in enterprise checkout flows.
📌 Key Takeaways
- Structure multi-step wizards as a Finite State Machine with explicit step validation guards.
- Add
novalidateto the form to prevent hidden fields from blocking step progression. - Use
<nav>andaria-current="step"to communicate stepper progress to assistive technologies. - Shift programmatic focus to the active step's
<legend tabindex="-1">upon transition. - Persist multi-step draft progress safely in
sessionStorageand clear it upon final order completion. - --