ComponentsFlows & wizards

Multi-step form

A long form split into steps on top of the Stepper: per-step validation with an error summary, conditional steps, draft saving and autosave, URL resume, a review step with Edit links, and a leave-page warning. Form-library agnostic — each step exposes validate().

Stepper · native <form> business/multi-step-form.tsxtheme · material · accent · density · font
  1. Step 1 of 4, Identity, Current
  2. Step 2 of 4, Contact
  3. Step 3 of 4, Contract
  4. Step 4 of 4, Review

Step 1 of 4Identity

Step 1 of 4: Identity

As written on the ID card.

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { MultiStepForm, MultiStepFormStepper, MultiStepFormStep, MultiStepFormReview, MultiStepFormFooter } from "@vamos-ui/react/business";

const steps = [
  { id: "identity", title: "Identity", validate: () => (name ? true : "Enter a name.") },
  { id: "tax", title: "Tax details", when: (ctx) => ctx.kind === "company" },
  { id: "review", title: "Review" },
];

<MultiStepForm steps={steps} context={{ kind }} onComplete={save}>
  <MultiStepFormStepper />
  <MultiStepFormStep id="identity">…</MultiStepFormStep>
  <MultiStepFormStep id="tax">…</MultiStepFormStep>
  <MultiStepFormStep id="review"><MultiStepFormReview sections={…} /></MultiStepFormStep>
  <MultiStepFormFooter submitLabel="Create employee" />
</MultiStepForm>

On a step change focus moves to the step heading, announced as "Step 3 of 6: Contract". On a validation error focus goes to the first invalid field (or the summary).

Enter in a single-line field goes to the next step instead of submitting the whole form. Back never loses entered values.

Examples

Conditional step

when(context) hides the Tax step for individuals; the Stepper count and "Step n of m" update.

  1. Step 1 of 3, Account type, Current
  2. Step 2 of 3, Tax details
  3. Step 3 of 3, Payout account

Step 1 of 3Account type

Step 1 of 3: Account type

Inside a Dialog

presentation="dialog" keeps the footer in flow; the compact Stepper fits narrow panels.

Drafts, autosave and URL resume

onSaveDraft + autosave show "Draft saved 2 minutes ago"; syncUrl mirrors the step in the query string; confirmLeave guards the tab.

  1. Step 1 of 3, Company, Current
  2. Step 2 of 3, Partner programme
  3. Step 3 of 3, Agreement

Step 1 of 3Company

Step 1 of 3: Company

Vertical with free navigation

Settings-style setup: the Stepper is a sticky side rail with descriptions and linear={false} allows jumping ahead.

  1. Step 1 of 3, StoreName and address, Current

Step 1 of 3: Store

Name and address

API

MultiStepForm

PropTypeDefault
steps
{ id, title, description?, optional?, when?(ctx), validate?() → true | string | Promise }. A string blocks Next and is shown as the step's error summary.
StepDef[]—
value / defaultValue / onValueChange
Current step id (controlled or not).
string—
context
Passed to when() of conditional steps.
C—
onComplete
Runs on Submit. Throw to show the error and stay on the step.
() => Promise<void> | void—
linear
Steps ahead are reachable only after the earlier ones pass.
booleantrue
onSaveDraft
Shows "Save draft" and the saved time.
() => Promise<void> | void—
autosave
Calls onSaveDraft on an interval (min 2 s).
{ intervalMs: number }—
presentation
page: sticky footer on an opaque surface. dialog: footer in flow.
"page" | "dialog""page"
orientation
Vertical puts the Stepper in a sticky side rail.
"horizontal" | "vertical""horizontal"
syncUrl
Mirror the step in ?step= (or your key) with history.replaceState.
boolean | { key: string }false
confirmLeave
beforeunload warning until the form completes.
booleanfalse
labels
Back / Next / Submit / Save draft / "Step {current} of {total}"…
Partial<MultiStepFormLabels>—

MultiStepFormStep

PropTypeDefault
id
Matches a StepDef id. Hidden (but mounted) while inactive, so uncontrolled values survive Back.
string—
hideHeading
Render your own heading; a visually hidden one keeps focus and the announcement.
booleanfalse
keepMounted
Unmount inactive steps instead.
booleantrue

MultiStepFormStepper

PropTypeDefault
variant
Stepper look; completed steps are clickable.
"numbered" | "compact" | "bar" | "dots""numbered"

MultiStepFormReview

PropTypeDefault
sections
Summary per step with an Edit link that jumps back.
{ step: string; title?: string; items: { label: string; value: ReactNode }[] }[]—

MultiStepFormFooter

PropTypeDefault
submitLabel
Label of the last step's primary button.
string"Submit"
start
Content at the start (Cancel).
ReactNode—

useMultiStepForm()

PropTypeDefault
returns
Drive the form from custom parts.
{ current, index, total, steps, next(), back(), goTo(id), saveDraft(), isFirst, isLast, status, submitting }—