Multi-Step Checkout — Free HTML CSS JS Form Snippet

Multi-Step Checkout · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Numbered progress stepper
An ordered list with a connecting track shows upcoming, active, and done states, recomputed from the current index.
Per-step validation gate
Continue is blocked until the current panel's required fields pass value and native checkValidity() checks.
Native validity for free
Using checkValidity() means the email field enforces email format without custom regex.
Focus-the-first-error
Invalid fields get a red ring and focus jumps to the first one, so the user knows exactly what to fix.
Forgiving error clearing
A delegated input listener removes the invalid state the instant the user starts correcting a field.
Values persist across steps
All panels stay mounted, so stepping back and forward never loses entered data.
Adaptive button label
The primary button reads Continue mid-flow and Place order on the last step, and Back disables on step one.
Animated success screen
Completing the order hides the form and reveals a green checkmark confirmation with a fade-in.

About this UI Snippet

Multi-Step Checkout — Stepper Form with Per-Step Validation and Success Screen

Screenshot of the Multi-Step Checkout snippet rendered live

Long forms scare buyers away. Splitting checkout into a few focused steps — contact, shipping, payment — lowers the perceived effort, lets you validate each part before moving on, and gives a clear sense of progress. This component is a complete multi-step checkout: a numbered progress stepper across the top, three sliding panels, per-step validation that blocks advancement until the current fields are valid, back and continue navigation, and a success screen when the order is placed. It is built in semantic HTML, CSS, and vanilla JavaScript with no form library.

The progress stepper

The three steps are an ordered list with a connecting track drawn by a ::before pseudo-element line behind the numbered circles. Each step has three visual states managed by classes: upcoming (grey circle), active (filled indigo circle, dark label), and done (green circle). As the user advances, the render() function recomputes every step's state from the current index — active for the current step, done for any step before it — so the stepper always reflects exactly where the user is. The colour transitions are CSS, so each state change animates smoothly.

One panel visible at a time

All three panels exist in the DOM at once; only the one matching the current index has the .active class and display: block, the rest are display: none. When the active panel changes, a short mscFade keyframe slides it in from the right with a fade, giving a sense of forward motion through the flow. Keeping every panel mounted means field values persist when the user steps back and forward — nothing is lost because nothing is destroyed.

Per-step validation that gates advancement

The heart of a good multi-step form is that you cannot skip ahead with invalid data. Clicking Continue calls validateStep(), which collects the current panel's input[required] elements and checks each with value.trim() !== '' plus the browser's native checkValidity() (so the email field enforces a valid email format for free). Invalid fields get an .invalid class — a red border and ring — and focus jumps to the first one. Only if every required field passes does the index advance. This validates one step at a time rather than dumping every error at the end.

Forgiving error clearing

Errors should not linger after the user fixes them. A single delegated input listener on the form removes the .invalid class from any field the moment the user starts typing in it. This is the forgiving pattern — strict on submit, lenient as the user corrects — which feels far better than leaving a red border until the next Continue click.

Adaptive navigation and the success screen

The Back button is disabled on the first step (you cannot go back from contact), and the primary button relabels itself: it reads "Continue" on steps one and two and "Place order" on the final step, computed in render() from whether the current index is the last panel. Placing the order adds a .complete class that hides the stepper and panels and reveals a success screen with an animated green checkmark and confirmation text. Because the state is index-driven, the whole flow is just three functions — render(), validateStep(), and the click handlers — with no tangled conditionals.

Customisation

Add or remove steps by editing the stepper <li> items and the matching .msc-panel blocks — the script reads them dynamically with querySelectorAll, so the counts stay in sync automatically. Mark which fields are required with the required attribute, swap the #6366f1 accent and #22c55e done/success green for your brand, and replace the hard-coded order total with a real value. To submit to a backend, replace the success block in the final-step branch with a fetch POST of the collected field values.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than tracing the step-gating logic by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how validateStep() leans on the browser's native checkValidity() to get email-format checking for free, and why keeping all three panels mounted (rather than creating/destroying them) is what lets field values survive going back a step. The same assistant can help optimize it, for instance asking whether the querySelectorAll re-scan of steps and panels on every render() call would still be efficient with many more steps, or whether the invalid-clearing input listener should be scoped per-field instead of delegated at the form level. It's also useful for extending the flow: ask it to add an inline order-summary sidebar that updates per step, wire real-time card-number formatting into the payment panel, or persist in-progress checkout state to sessionStorage so a refresh doesn't lose it. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "multi-step checkout" form in plain HTML, CSS, and JavaScript using an index-driven stepper — no form library, no framework.

Requirements:
- A numbered progress stepper (contact, shipping, payment) with a connecting track line behind the step circles, where each step visually shows one of three states — upcoming, active, or done — computed purely from comparing its index to the current step index.
- Three form panels, one per step, all mounted in the DOM simultaneously; only the panel matching the current step index is visible (toggled via a class, not removed from the DOM), and switching panels must play a subtle slide-and-fade transition.
- Clicking "Continue" must first validate only the current panel's required fields, checking both that each is non-empty and that it passes the browser's native checkValidity() (so an email field enforces real email format with no custom regex); if any field fails, block advancing, visually mark the invalid fields, and move focus to the first one.
- Fields must clear their invalid state the moment the user starts correcting them, via one delegated input listener rather than per-field listeners.
- A Back button must be disabled on the first step, and the primary button's label must change to "Place order" only on the final step.
- Reaching the final step successfully and clicking Place order must hide the stepper and form entirely and reveal a distinct success view with a confirmation message.
- Adding or removing a step (a stepper item plus a matching panel) must require no changes to the JavaScript logic — the code must read the step/panel count dynamically.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA checkout card renders on step 1 (Contact) with a three-step progress stepper and a disabled Back button.
  2. 2
    Click ContinueIf required fields are valid the form advances, the panel slides in, and the completed step turns green; invalid fields get a red ring and focus.
  3. 3
    Step back and forwardUse Back to return to a previous step — your entered values are preserved because panels stay mounted.
  4. 4
    Place the orderOn the final step the button reads "Place order"; clicking it reveals an animated success screen.
  5. 5
    Add or remove stepsEdit the stepper <li> items and matching .msc-panel blocks; the script reads them dynamically so counts stay in sync.
  6. 6
    Wire to a backendReplace the success branch with a fetch POST of the collected field values, keeping the same stepper and validation.

Real-world uses

Common Use Cases

E-commerce checkout
Split contact, shipping, and payment into focused steps to reduce cart abandonment — pair with an order summary and a promo code input.
Signup and onboarding wizards
Collect account details over a few steps with validation between them; for a generic flow see the multi-step form.
Booking and reservation flows
Guide users through details, date, and payment with clear progress and back navigation.
Subscription and plan setup
Combine plan choice, billing, and confirmation; complements a plan selector on the first step.
Application and survey forms
Break a long application into digestible sections, validating each before advancing.
Learning multi-step UX
A reference for index-driven panels, per-step validation, and the forgiving error-clearing pattern.

Got questions?

Frequently Asked Questions

Add another <li class="msc-step"> to the stepper and a matching <div class="msc-panel" data-panel="3"> to the panels. The script reads steps and panels with querySelectorAll at load, so it adapts to the new count automatically — the last-step detection (current === panels.length - 1) and the stepper states all keep working without code changes.

validateStep() selects only the current panel's input[required] elements, so each step validates just its own fields. Each is checked for a non-empty trimmed value and the browser's native checkValidity(), which enforces type rules like email format and pattern attributes. Add the required attribute to any field you want to gate the step, and use input types (email, tel) or pattern for format rules.

Yes. All panels stay in the DOM the whole time — only the active one is shown via a class. Going back simply changes which panel is visible; the inputs and their values are never removed or re-created, so everything the user typed is still there when they return.

In the nextBtn handler's final-step branch (where it currently adds the .complete class), first gather the field values (e.g. with new FormData(form) or by reading each input), then await a fetch POST to your endpoint. Show the success screen on a successful response, or surface an error toast on failure. You can also disable the button and show a loading state during the request.

Keep the current step index and the form values in state. Render the active panel based on the index, and bind each step's stepper state (active/done) to comparisons with the index. Move validateStep() into a function that checks the current step's values before incrementing the index. Native checkValidity() still works on refs; or validate against your own schema. The CSS — stepper, panel fade, invalid styles — ports unchanged.