Vertical Stepper — Free HTML CSS JS Form Snippet

Vertical Stepper · Forms · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

0fr grid height animation
Panels transition grid-template-rows: 0fr → 1fr, animating unknown content height with no JS measurement or max-height hacks.
Number-to-check dot morph
Each dot cross-fades its number out and scales a checkmark in as the background sweeps grey → green.
Filling connector rail
Connector lines fill top-down with a scaleY transform, pouring progress toward the next step on the compositor.
Two-integer state model
current and maxReached drive every class via one update() — the DOM is a pure function of the state.
Jump-back editing
Completed step titles are clickable to re-open, with progress preserved when you return.
Single delegated listener
Continue, Back, and title clicks route through one root listener via closest() — step count never changes the JS.
Completion banner
Walking past the last step reveals an animated success banner, the natural submit point for collected data.
Focus-styled inputs
Inputs carry accessible labels and an accent focus ring consistent across the flow.

About this UI Snippet

Vertical Stepper — Material-Style Multi-Step Form with Expanding Panels and Progress Rail

Screenshot of the Vertical Stepper snippet rendered live

The vertical stepper is the onboarding pattern of choice when steps have real content — unlike a horizontal stepper, each step's form expands inline beneath its own title, so the user always sees where they are, what is done, and what is next in one column. This component implements the full Material-style pattern in HTML, CSS, and vanilla JavaScript: numbered dots that morph into green checkmarks, connector lines that fill as you advance, smoothly animating collapsible panels, back navigation, jump-back editing of completed steps, and a completion banner.

Animating height with the 0fr grid trick

Collapsible content whose height is unknown is the classic CSS problem: height: auto cannot be transitioned. This stepper uses the modern solution — each panel is a one-cell grid transitioning grid-template-rows between 0fr and 1fr, with the inner element set to overflow: hidden. The browser interpolates the fractional row from zero to content height, so panels expand and collapse smoothly with zero JavaScript measurement — no scrollHeight reads, no hard-coded max-heights that break when content changes. It is the cleanest height animation technique shipping in all modern browsers.

The rail: dots, checks, and filling connectors

Each step's left rail holds a 30px dot and a 2px connector line. The dot renders both the step number and a checkmark SVG stacked on top of each other; the done state fades the number out and scales the check in, while the dot's background transitions from grey to green — one CSS state change, two cross-fading layers. The connector line contains an inner <i> filled with the accent colour and scaled with transform: scaleY(0→1) from the top, so completing a step visibly "pours" progress down toward the next one. Scaling a transform instead of animating height keeps the fill on the compositor.

State: one index, three classes

The JavaScript tracks two numbers: current (the open step) and maxReached (the furthest completed step). update() derives every visual from them — active for the open step, done for completed steps behind the cursor — so the DOM is a pure function of two integers. Continue advances both; Back moves current down without losing maxReached, which is what enables jump-back editing: clicking the title of any previously completed step re-opens it, and because maxReached is preserved, its checkmark returns the moment you navigate away again.

Event delegation for all controls

A single click listener on the root handles Continue, Back, and title clicks via closest() checks, rather than binding listeners per button. This keeps the wiring at three branches regardless of step count — add a fourth step to the markup and no JavaScript changes are needed beyond it simply working.

Completion state

When current walks past the last step, every step shows as done and a green completion banner fades in with a small rise animation. In a real signup flow this is where you would submit the accumulated form data — the demo keeps inputs uncontrolled so you can read them all at the end with a FormData pass or per-input queries.

Customisation

Add steps by copying a .vst-step block (the last step omits the connector line); the delegation and state logic adapt automatically. Add per-step validation by gating the Continue branch — check the step's inputs and refuse to advance with an error style, similar to the inline validation form. Swap the indigo accent and green done-colour tokens to match your brand.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Instead of tracing the grid trick by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why animating grid-template-rows between 0fr and 1fr solves the "unknown content height" problem that a plain height transition cannot, and why the inner wrapper still needs overflow hidden even though the outer grid row is already collapsing. It's worth asking about the two-integer state model too — have it explain precisely why current alone can't distinguish "behind me because I finished it" from "behind me because I pressed Back," and what would break if maxReached were removed. For extending it, have it add per-step validation that blocks Continue until required inputs are filled, a progress percentage readout derived from current and steps.length, or a way to persist answers across a page reload. 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 Material-style vertical multi-step form (stepper) in plain HTML, CSS, and vanilla JavaScript with no libraries, using the CSS grid-template-rows trick to animate panel height without any JavaScript measurement.

Requirements:
- Several sequential steps, each with a numbered dot, a connector line to the next step, a clickable title, and a collapsible panel containing that step's form fields.
- Each step's collapsible panel must be implemented as a single-cell CSS grid whose grid-template-rows transitions between 0fr (collapsed) and 1fr (expanded), with its inner content wrapper set to overflow hidden — do not use height, max-height, or any JavaScript scrollHeight measurement to animate the expand/collapse.
- Track exactly two numeric state values in JavaScript: the currently open step index, and the furthest step index the user has ever reached. Every visual state (which step is active, which are marked done) must be derived purely from comparing indices against these two numbers, not from separate flags stored per step.
- Each step's dot must visually morph between showing its number and showing a checkmark: cross-fade the number out and scale a checkmark icon in when the step becomes done, with the dot's background color transitioning at the same time.
- The connector line between two dots must fill downward with a scaleY transform (from 0 to 1, anchored at the top) when the upper step becomes done, rather than animating height or width.
- Clicking a "Continue" button must advance the current step and update the furthest-reached value; clicking "Back" must move the current step backward without ever reducing the furthest-reached value.
- Clicking the title of any step that is at or before the furthest-reached value must jump directly back to that step for editing, and its done state must be restored automatically once the user moves forward again.
- Handle all Continue/Back/title clicks through a single delegated click listener on the stepper's root container rather than binding a separate listener to every button.
- When the user continues past the final step, show a distinct completion banner and mark every step as done.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Paste the HTML, CSS, and JSA three-step vertical stepper renders — Account details is open with its form expanded, steps 2 and 3 sit collapsed and grey below.
  2. 2
    Click ContinueThe current panel collapses, the dot turns green and cross-fades its number into a checkmark, the connector line fills downward, and the next step expands.
  3. 3
    Go backBack re-opens the previous step without losing progress — its checkmark returns as soon as you move forward again.
  4. 4
    Jump-edit a done stepClick the title of any completed step to re-open it directly; maxReached remembers how far you had gotten.
  5. 5
    Finish the flowContinue past step 3 — every dot shows a check and the green "All steps completed" banner animates in.
  6. 6
    Extend and validateCopy a .vst-step block to add steps; gate the Continue handler with input checks to add per-step validation.

Real-world uses

Common Use Cases

SaaS onboarding flows
Account → workspace → invite is the canonical signup; pair with an onboarding checklist widget after setup.
Checkout pipelines
Address, shipping, and payment as vertical steps — compare the multi-step checkout variant.
KYC and application forms
Long regulated forms broken into digestible, resumable sections with visible progress.
Setup wizards
Product configuration where earlier answers stay reviewable via jump-back editing — see also the progress wizard.
Course and tutorial flows
Lesson sequences where completed steps collapse but remain revisitable.
Learning the grid trick
A working reference for the 0fr/1fr height animation, state-derived classes, and event delegation.

Got questions?

Frequently Asked Questions

Each panel is display: grid with grid-template-rows: 0fr, transitioning to 1fr when the step is active. Fractional rows are interpolable, so the browser animates the row from zero to the content's natural height itself. The inner wrapper needs overflow: hidden so content clips during the transition. This replaces the old max-height hack, which needed a magic number and animated at the wrong speed when content was shorter than it.

current alone cannot distinguish "behind me because I completed it" from "behind me because I pressed Back". maxReached records the furthest completion, so update() can mark steps done when they are below maxReached but not currently open. That is what lets Back and jump-to-title re-open a step for editing while its completed status (checkmark, filled connector) is restored the moment you move on.

In the Continue branch, query the active step's inputs and check them before advancing: const bad = steps[current].querySelector('.vst-input:invalid, .vst-input[required][value=""]'). Use the Constraint Validation API — mark inputs required, call input.reportValidity(), and only increment current when every field passes. Add an error class that turns the border red for inline feedback.

Copy a .vst-step block, update its number, data-step, and content, and place it in order — omit the .vst-line on whichever step is last. The script derives steps from a querySelectorAll and the delegated listener handles any count, so no JavaScript edits are needed. The completion banner triggers when current reaches steps.length, which recalculates automatically.

Hold current and maxReached in state and render steps from a config array, deriving active/done classes per index — update() disappears into the template. Handlers become setState calls on button clicks. Keep field values in controlled state per step so you can validate before advancing and submit everything at the end. The 0fr grid transition, dot morph, and connector fill are pure CSS and port unchanged.