Vertical Stepper — Free HTML CSS JS Form Snippet
Vertical Stepper · Forms · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Vertical Stepper — Material-Style Multi-Step Form with Expanding Panels and Progress Rail

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:
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
- 1Paste 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.
- 2Click 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.
- 3Go backBack re-opens the previous step without losing progress — its checkmark returns as soon as you move forward again.
- 4Jump-edit a done stepClick the title of any completed step to re-open it directly; maxReached remembers how far you had gotten.
- 5Finish the flowContinue past step 3 — every dot shows a check and the green "All steps completed" banner animates in.
- 6Extend 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
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.