Inputs

Multi-step Form

A multi-step form container with direction-aware sliding transitions and a switchable progress indicator style.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/multi-step-form.en.json?t=<install token>"

New project or installation trouble? Start here →

Multi-step Form is a React multi-step form (form wizard) container built with Motion (motion/react) and Tailwind CSS. You declare steps with MultiStepFormStep, steps slide in the direction you move, and the progress indicator comes in bar, dots or numbered-steps styles. The component handles step flow and transitions only; fields and validation stay in your own form logic.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<MultiStepForm/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • SaaS sign-up split into account, company details and preferences
  • Checkout broken into shipping address, delivery method and payment review
  • New-user onboarding surveys that ask one or two questions per screen
  • Long registration or application forms that read better in sections
  • Not a good fit: if you only need to show progress and the content does not swap inside one container, use Stepper

Installation

npx webberui@latest add "https://webberui.com/r/multi-step-form.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/multi-step-form.

Usage

Declare each step with <MultiStepFormStep>; the container makes a direction-aware sliding transition based on the current step and renders the progress indicator and the back / next control bar automatically.

import {
  MultiStepForm,
  MultiStepFormStep,
} from "@/components/ui/multi-step-form";

<MultiStepForm variant="steps" onComplete={() => console.log("done")}>
  <MultiStepFormStep label="Account">{/* ...fields */}</MultiStepFormStep>
  <MultiStepFormStep label="Profile">{/* ...fields */}</MultiStepFormStep>
  <MultiStepFormStep label="Confirm">{/* ...summary */}</MultiStepFormStep>
</MultiStepForm>;

Custom control bar

Set controls to false and use the useMultiStepForm() hook to assemble your own navigation anywhere (so you can validate before calling next()):

import { useMultiStepForm } from "@/components/ui/multi-step-form";

function Footer() {
  const { back, next, isFirst, isLast } = useMultiStepForm();
  return (
    <div>
      <button onClick={back} disabled={isFirst}>
        Back
      </button>
      <button onClick={next}>{isLast ? "Submit" : "Next"}</button>
    </div>
  );
}

Controlled mode

Pass step and onStepChange to take full control of the current step — ideal for gating steps behind form validation.

const [step, setStep] = React.useState(0);

<MultiStepForm step={step} onStepChange={(next) => setStep(next)}>
  {/* ...steps */}
</MultiStepForm>;

Examples

Only advance when the step is valid

In controlled mode, onStepChange receives the direction. When moving forward (direction === 1), check the current step and skip updating step if it fails. Pressing "Done" on the last step calls onComplete and does not go through onStepChange.

"use client";

import * as React from "react";
import {
  MultiStepForm,
  MultiStepFormStep,
} from "@/components/ui/multi-step-form";

export function SignupForm() {
  const [step, setStep] = React.useState(0);
  const [email, setEmail] = React.useState("");
  const [error, setError] = React.useState("");

  return (
    <MultiStepForm
      step={step}
      onStepChange={(next, direction) => {
        if (direction === 1 && step === 0 && !email.includes("@")) {
          setError("Enter a valid email address");
          return;
        }
        setError("");
        setStep(next);
      }}
      onComplete={() => console.log("submit", { email })}
    >
      <MultiStepFormStep label="Account">
        <input
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          placeholder="you@company.com"
          className="w-full rounded-lg border px-3 py-2 text-sm"
        />
        {error ? <p className="mt-2 text-sm text-red-600">{error}</p> : null}
      </MultiStepFormStep>
      <MultiStepFormStep label="Company">{/* name, team size */}</MultiStepFormStep>
      <MultiStepFormStep label="Review">{/* summary */}</MultiStepFormStep>
    </MultiStepForm>
  );
}

Progress bar style with custom button text

variant="bar" shows the current step title and an "x / y" counter; the three label props change the built-in controls.

<MultiStepForm
  variant="bar"
  backLabel="Previous"
  nextLabel="Continue"
  completeLabel="Place order"
  onComplete={placeOrder}
>
  <MultiStepFormStep label="Shipping">{/* ... */}</MultiStepFormStep>
  <MultiStepFormStep label="Delivery">{/* ... */}</MultiStepFormStep>
  <MultiStepFormStep label="Payment">{/* ... */}</MultiStepFormStep>
</MultiStepForm>

Start from a given step

In uncontrolled mode, defaultStep sets the initial step, for example when a user comes back to finish a saved draft. Out-of-range indexes are clamped to a valid value.

<MultiStepForm defaultStep={savedDraft.step} variant="dots">
  {/* ...steps */}
</MultiStepForm>

Props

MultiStepForm

PropTypeDefaultDescription
childrenReactNode—The step declarations, all <MultiStepFormStep>
stepnumber—Controlled current step index; when omitted, it is uncontrolled
defaultStepnumber0Initial step index in uncontrolled mode
onStepChange(step: number, direction: 1 | -1) => void—Fires when the step changes, with the new index and the direction
onComplete() => void—Fires when "Done" is pressed on the last step
variant"bar" | "dots" | "steps" | false"steps"Progress indicator style; false hides it
controlsbooleantrueWhether to render the built-in control bar
backLabelstring"Back"Text of the back button
nextLabelstring"Next"Text of the next button
completeLabelstring"Done"Text of the final-step button

MultiStepFormStep

PropTypeDefaultDescription
labelstring—Step title shown in the progress indicator
childrenReactNode—Step content
classNamestring—Appended to the step container's className

useMultiStepForm()

Returns { step, total, direction, isFirst, isLast, goTo, next, back, reducedMotion } for use in a custom control bar or progress indicator.

How it works

  • Three progress styles: bar progress bar, dots (the current dot stretches into a pill), and steps numbered steps (completed ones get a checkmark and the connecting lines colour in with the progress).
  • Direction awareness: going forward, the new step slides in from the right and the old one exits to the left; going back is the reverse. The direction is derived during render from the previous index, and is correct in both controlled and uncontrolled modes.
  • The viewport uses overflow-hidden to clip the horizontal slide, and popLayout takes the exiting step out of flow so the entering step decides the container height, preventing the height from collapsing during the transition.

Accessibility

  • Steps in the progress indicator that are current or already visited can be clicked to jump back (goTo), while steps not yet reached are disabled, preserving the fill-in order.
  • The current step is marked aria-current="step"; the progress bar provides role="progressbar" and aria-valuenow.
  • Step changes are announced as "step x of y" through an aria-live="polite" region.
  • The built-in control bar uses native buttons that are keyboard-operable with a focus-visible ring; the back button is disabled on the first step.
  • When the user has "reduce motion" enabled at the system level, the slide degrades to a brief fade in / fade out, and the progress fill jumps straight into position.

FAQ

Does it work with the Next.js App Router?

Yes. The file already starts with "use client", so import it from a Server Component page as usual; any wrapper that holds state or event handlers needs its own "use client" too.

Can I use it with a form library such as React Hook Form?

Yes. The component does not manage field values, so wrap the whole MultiStepForm in your <form> or form provider and switch steps after validation, either in controlled mode or by calling next() from useMultiStepForm(). The built-in control buttons are type="button", so they never submit the form by accident.

Why are other elements inside MultiStepForm not rendered?

The container only picks up direct MultiStepFormStep children and ignores everything else. Put headings and helper text inside a step's content or outside MultiStepForm.

Do screen readers hear the step change?

Yes. An aria-live="polite" region announces "step x / y" with the step title, and the current step carries aria-current="step". With the OS "reduce motion" setting on, the slide becomes a short fade.

What are the dependencies?

motion, lucide-react, clsx and tailwind-merge; see the installation section above.