Skip to content
Template Empire
Browse components

Multi-step Progress Form

A free multi step form with real validation, a generated review, and animated panels.

Step 2 of 4: How to reach you

  1. About the project
  2. How to reach you
  3. Timing
  4. Review

How to reach you

One reply from a person, never a mailing list.

Customise

22px
0.3s
24px

Code

<InputMultistepProgress steps={steps} initialValues={draft} initialStep={1} onSubmit={handleSubmit} />

Multi-step Progress Form is a free multi step form react component for Next.js. One native form renders one step at a time: Next validates the step you are on, refuses to advance while something is missing or malformed, and puts focus on the first control that needs attention. Back preserves everything you have typed, and a generated review of every answer stands between you and submitting.

The panel height is animated from a single ResizeObserver measurement, batched into an animation frame so reads and writes are never interleaved, and switched on only after the first natural layout so nothing shifts as the form arrives. Where ResizeObserver is unavailable the panel simply keeps its natural height. Validation is stated in text beside the field and once in a polite summary, the error emphasis is a single restrained nudge rather than a shake or a loop, and with reduced motion enabled every swap is instant while validation, values, review, focus and submission behave exactly the same.

Install

Add the @te registry to your components.json once:

{
  "registries": {
    "@te": "https://templateempire.io/r/{name}.json"
  }
}

Then install the component:

npx shadcn@latest add @te/input-multistep-progress

Usage

import { InputMultistepProgress } from "@/components/input-multistep-progress";

const steps = [
  {
    id: "about",
    title: "About you",
    fields: [
      { id: "name", label: "Your name", type: "text", required: true, autocomplete: "name" },
      { id: "email", label: "Email address", type: "email", required: true, autocomplete: "email" },
    ],
  },
  {
    id: "detail",
    title: "Your message",
    fields: [{ id: "message", label: "How can we help?", type: "textarea", required: true }],
  },
];

export default function Page() {
  return (
    <InputMultistepProgress
      steps={steps}
      onSubmit={(values) => {
        console.log(values);
      }}
    />
  );
}

Props

PropTypeDefaultDescription
stepsReadonlyArray<{ id: string; title: string; description?: string; fields: ReadonlyArray<{ id: string; label: string; type: "text" | "email" | "textarea"; required?: boolean; autocomplete?: string }> }>requiredTwo to five steps. Field ids must be unique across the whole form, not just within a step; duplicates are dropped, first wins. A step with no usable field is dropped.
initialValuesReadonly<Record<string, string>>{}Starting values, read once on mount. Keys with no matching field are ignored, and later changes to this prop never overwrite what has been typed.
initialStepnumber0Zero-based stop to open on, read once on mount and clamped to the stops that exist. Resumption also stops at the first step initialValues does not answer, so a draft can never open past an unchecked step. The generated review is the last stop, so a complete draft can resume on it.
onSubmit(values: Readonly<Record<string, string>>) => voidrequiredCalled with every field value once the review step is confirmed. Never called from an intermediate step.
direction"horizontal" | "vertical""horizontal"The axis step panels travel along. Next and Back use opposite directions on that axis.
distancenumber22How far a panel travels as it swaps, in CSS pixels, clamped to 0-48. Zero gives a pure crossfade.
progressStyle"bar" | "steps""steps"How progress is drawn: steps renders a labelled marker per stop, bar renders a single scaled fill. The step count is stated in text either way.
heightDurationnumber0.3How long the panel takes to settle to its new height, in seconds, clamped to 0-0.7. Zero makes the resize instant.
radiusnumber24Form and panel corner radius in CSS pixels, clamped to 0-44.
classNamestringundefinedAdditional classes merged onto the form with cn().

Customisation

Multi-step Progress Form declares five tokens of its own, each falling back to a global semantic token, so it retints from an ancestor without touching the component source. Geometry and timing come from props rather than tokens, because they are clamped: --multistep-accent fills the stop you are on, the joins behind it and the primary control, --multistep-accent-foreground is the ink inside that fill and moves with it, --multistep-accent-text tints the completed stop's tick and is a separate knob because a fill rung is too dark to read as text, --multistep-panel is the form and review surface, and --multistep-error is the validation accent.

:root {
  --multistep-accent: var(--chart-1);
  --multistep-accent-foreground: var(--background);
  --multistep-accent-text: var(--chart-2);
  --multistep-panel: var(--muted);
  --multistep-error: oklch(0.62 0.21 25);
}

<InputMultistepProgress steps={steps} onSubmit={handleSubmit} progressStyle="bar" radius={8} />

Frequently asked questions

Is Multi-step Progress Form free for commercial use?
Yes. Multi-step Progress Form is MIT licensed, so you can use it in personal and commercial projects without attribution.
How is the panel height animated without causing layout shift?
A single ResizeObserver watches the panel content. The measurement is read in the observer callback and the height written in an animation frame, so a read and a write are never interleaved and the browser's ResizeObserver loop warning cannot fire. The height transition itself is switched on only on the frame after the first height is committed, so the form's arrival uses the browser's own natural layout and nothing animates from one value to the same value. Where ResizeObserver is unavailable no height is written at all and the panel keeps its natural height.
Is Multi-step Progress Form accessible for keyboard and screen reader users?
Yes. Every control has a real visible label, an optional autocomplete value, a required state, and aria-invalid with its message associated through aria-describedby when validation fails. The primary control is a real submit button, so Enter inside any field invokes Next on an intermediate step and submits only on review. After a successful advance focus moves to the new step heading, which is programmatically focusable but not a tab stop; after a failed one it moves to the first invalid control. Errors are stated in text beside the field and once in a polite live region, and no value you have entered is announced back until you reach the review step.

Related components

Want the full page this belongs to?Template Empire builds complete Next.js templates with this motion system throughout. Browse the paid templates that pair with these free components.Explore Template Empire templates