Multi-Step Progress Wizard — HTML CSS JS Snippet
Multi-Step Progress Wizard · Navigation · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Multi-Step Progress Wizard — Animated Step Indicator, Connector Fill & Plan Selector

A multi-step wizard breaks a long form into smaller, focused stages that feel manageable. Each step shows one logical group of fields — personal info, security, plan selection, confirmation — and a persistent progress indicator at the top communicates how far along the user is. Research consistently shows that multi-step forms have higher completion rates than long single-page forms because the cognitive load per screen is lower and progress feels visible and achievable.
Step indicator architecture
The step indicator is a horizontal flex row: four .step elements separated by .step-line spacers. Each step contains a circle icon and a label; only the active step shows its label text (via display:none on .step-info and display:block on .step.active .step-info). This keeps the indicator compact on small screens while showing helpful context on the active step.
Connector line fill animation
Each .step-line has a child .step-line-fill div. When a step is completed, JavaScript sets fill.style.width = '100%'. Because the CSS defines transition: width 0.4s ease on the fill, the line smoothly grows from left to right — visually "unlocking" the path to the next step. This technique requires no canvas or SVG — just a block div inside an overflow:hidden container.
Done state: swap icon via CSS
Completed steps show a checkmark instead of their original icon. This is achieved with two sibling SVGs: .step-icon (the step's purpose icon) and .check-icon (a checkmark). By default .check-icon is display:none. When the parent gets the .done class, CSS rules flip them: .step.done .step-icon { display:none } and .step.done .check-icon { display:block }. No DOM manipulation required — the icons are always in the HTML, CSS controls which is visible.
Panel transitions
Each form section is a .panel div; only the one with .active has display:block. A CSS @keyframes fadeIn animation on .panel.active slides the panel in from 12px to the right with an opacity fade over 250ms, giving a sense of forward motion when advancing steps. Going backwards could use a mirrored animation (translateX from -12px) which you can add by swapping the class on the panel before setting active.
Plan selector with :has() CSS
The plan selection uses <label> wrappers containing a hidden radio input. .plan-card:has(input:checked) applies the selected border and background when the radio inside is checked — pure CSS selection state with no JavaScript listener. A JavaScript click handler also syncs a .selected class for browsers that don't support :has() yet, providing a robust progressive enhancement approach.
JavaScript state machine
The current integer tracks the active step (1–4). updateWizard() is a pure render function: it iterates 1–TOTAL, assigns active/done/neutral classes to each step, activates the matching panel, and sets line fill widths. Both nextStep() and prevStep() simply increment/decrement current then call updateWizard(). This single-state-variable + pure-render pattern maps directly to React's useState hook.
Success state animation
The final panel shows a green ring with a checkmark that scales in from 0 using @keyframes pop with a spring cubic-bezier (0.175, 0.885, 0.32, 1.275). This overshoot easing is the same used in iOS confirmation animations — it makes the success feel earned and satisfying.
Button state management
The Back button has the HTML disabled attribute toggled by btnBack.disabled = current === 1. The Next button's label changes to "Finish" with a checkmark icon when on the penultimate step, and the button is disabled on the last step. All visual states flow from the single current integer — no scattered class toggles.
React integration
Use useState(1) for the current step. Wrap each panel in a component: <ProfileStep />, <SecurityStep />, <PlanStep />, <SuccessStep />. Render only the active component. The step indicator maps over a STEPS config array to render circles and labels. Pass onNext and onBack callbacks down to each step component, or manage navigation in the parent wizard component.
Accessibility
The step indicator should use role="list" on the steps container and role="listitem" on each step. The active step should have aria-current="step". The panels should use aria-live="polite" so screen readers announce the new panel content when it appears. Disabled buttons already communicate their state via the HTML disabled attribute.
See also the stepper snippet for a simpler number-based indicator, the multi-step form snippet for a form-focused variant, and the circular steps snippet for a radial progress design.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You don't have to trace every class toggle in updateWizard by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how a single current integer drives the step circles, the connector fill widths, the active panel, and the Back/Next button states all from one loop in updateWizard, or how the plan-card selection uses the CSS :has(input:checked) selector alongside a JavaScript fallback for browsers without it. The same assistant can help optimize it, for instance checking whether updateWizard's full re-render on every step change is wasteful compared to diffing only the two steps that changed, or whether the inline SVG icons duplicated in every step should be deduplicated with symbol/use references. It's equally useful for extending the wizard: ask it to add per-step field validation that blocks nextStep() until required inputs are filled, animate backward navigation with a mirrored slide direction, or persist in-progress answers to sessionStorage so a refresh doesn't lose progress. 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 multi-step signup wizard in plain HTML, CSS, and JavaScript with no framework and no libraries.
Requirements:
- A horizontal step indicator with one circular node per step (containing a purpose icon and a separate checkmark icon layered in the same node), connected by thin line segments between adjacent nodes. Only the active step's text label should be visible; other steps show only their icon.
- Each connector line segment must contain a child fill element that starts at zero width and animates to 100% width via a CSS transition on width when the step before it is completed, visually "unlocking" progress toward the next step.
- A completed step's node must swap from its purpose icon to the checkmark icon purely through CSS display toggles driven by a done class — both icons must already exist in the markup, with only visibility switched.
- One panel per step, each holding that step's form fields, with only the active step's panel displayed; switching the active panel must play a CSS keyframe animation that fades in and slides in slightly from one side.
- A single current integer step variable and one pure render function that, each time it runs, loops over every step and derives that step's classes (done, active, or neither), the matching panel's visibility, and each connector fill's width purely from comparing the step's position to current — no other code should ever set these classes directly.
- A plan-selection panel using label-wrapped radio inputs where the visually selected card is driven by the CSS :has(input:checked) selector, plus a JavaScript click handler that also toggles a selected class as a fallback for browsers without :has() support.
- Back and Next buttons wired to decrement/increment current and re-run the render function, with Back disabled on the first step, and Next's label and icon changing to "Finish" specifically on the second-to-last step.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
- 1Copy the full HTML structureThe wizard needs all four .panel divs and the .steps indicator in the same container. Do not separate them — the JS targets all elements by ID.
- 2Adjust step countTo add or remove steps: add/remove .step and .step-line elements in .steps, add/remove .panel divs, and update the TOTAL constant in the JS.
- 3Add the CSS and JSPaste both blocks. The JS updateWizard() function is the single source of truth — it derives all visual state from the current integer variable.
- 4Customise panel contentReplace the content inside each .panel with your own form fields. The wizard navigation works independently of what is inside each panel.
- 5Add validation before advancingIn nextStep(), add a validation check before incrementing current. Return early if validation fails, and show an error message inside the current panel.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Use useState(1) for current step. Map a STEPS config array to render the indicator. Render only the active panel component. Pass onNext/onBack as props or use a context.
In nextStep(), run your validation logic first. If it fails, show an error inside the current panel and return early without incrementing current.
Add an onclick to each .step that calls goToStep(n). Only allow jumping to completed steps (where i < current) to prevent skipping required fields.
Track a direction variable (next/prev). In fadeIn keyframe, use translateX(12px) for forward. Add a fadeInBack animation with translateX(-12px) and apply it when direction is prev.
Open the Export menu (or the Test Exports preview) in the snippet toolbar. It generates a plain React component, a React + Tailwind version where the step-indicator and panel styles become utility classes, a Vue 3 single-file component with the step navigation in script setup, and an Angular standalone component. Each converter preserves the markup, the connector-fill animation, and the panel transitions, so the wizard behaves identically across React, Vue, and Angular. Hold the current step and form values in component state and pass the plan options in as a prop or input instead of hardcoding the panels.