affectUI › Components › Inputs

Stepper Form

A multi-step flow for onboarding and setup – steps that fill and check off, sliding steps, a card that morphs to each step, inline validation and a success state.

Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/stepper-form.json · Import from @/components/ui/stepper-form

When to use

  • Onboarding, checkout or setup wizards where one long form would feel like too much at once
  • Flows whose later steps depend on earlier answers, ending in a review before anything is created

When not to use

  • Two or three fields – a single form is faster
  • Settings people jump around in freely – use tabs or sections instead

Usage

import { StepperForm, StepperField, StepperInput, StepperTags, StepperChoices, StepperReview, type Step } from "@/components/ui/stepper-form";

type Values = { name: string; slug: string; invites: string[]; plan: string };

const steps: Step<Values>[] = [
  {
    id: "workspace",
    title: "Workspace",
    heading: "Name your workspace",
    render: ({ values, setValues }) => (
      <>
        <StepperField name="name" label="Workspace name">
          <StepperInput value={values.name} onChange={(e) => setValues({ name: e.target.value, slug: slugify(e.target.value) })} />
        </StepperField>
        <StepperField name="slug" label="Workspace URL">
          <StepperInput prefix="acme.app/" value={values.slug} onChange={(e) => setValues({ slug: e.target.value })} />
        </StepperField>
      </>
    ),
    validate: (v) => ({ name: v.name.trim() ? undefined : "Give your workspace a name" }),
  },
  {
    id: "team",
    title: "Team",
    render: ({ values, setValues }) => (
      <StepperField name="invites" label="Email addresses" optional>
        <StepperTags value={values.invites} onChange={(invites) => setValues({ invites })} />
      </StepperField>
    ),
  },
  {
    id: "plan",
    title: "Plan",
    render: ({ values, setValues }) => (
      <StepperChoices label="Plan" value={values.plan} options={plans} onChange={(plan) => setValues({ plan })} />
    ),
  },
  {
    id: "review",
    title: "Review",
    render: ({ values, goTo }) => (
      <StepperReview
        onEdit={goTo}
        rows={[
          { label: "Workspace", value: values.name, step: "workspace" },
          { label: "Plan", value: values.plan, step: "plan" },
        ]}
      />
    ),
  },
];

export function Onboarding() {
  return (
    <StepperForm<Values>
      steps={steps}
      initialValues={{ name: "", slug: "", invites: [], plan: "pro" }}
      submitLabel="Create workspace"
      onComplete={(values) => api.createWorkspace(values)}
    />
  );
}

Props

NameTypeDefaultDescription
stepsStep<V>[]{ id, title, heading?, description?, render(ctx), validate?(values) } – render gets values, setValues, errors, goTo and next; validate returns messages by field name
initialValuesVValues shared by all steps, so Back and Edit keep what was entered
onComplete(values: V) => void | Promise<void>Runs on the last step; a promise shows the submitting label until it settles, a thrown error is shown next to the button
initialStepnumber0Index of the first step shown
stepnumberControlled step index; pair with onStepChange
onStepChange(step: number) => voidCalled whenever the step changes
submitLabelstring"Finish"Label of the button on the last step
submittingLabelstring"Creating…"Label while onComplete runs
success(values: V, reset: () => void) => ReactNodeReplaces the steps once done – StepperSuccess gives the popping check, a heading and a line
classNamestringExtra class on the root
StepperField{ name, label, hint?, optional?, children }Label, control and inline message for the field name in validate; the control shakes when Continue finds a problem
StepperInputInputHTMLAttributes & { prefix? }A text field wired to its StepperField; prefix shows fixed text like "acme.app/"
StepperTags{ value, onChange, validate?, max?, placeholder? }Entries as chips – emails by default; Enter, comma or a pasted list add, × or Backspace remove
StepperChoices{ value, onChange, options, label }Selectable cards { value, title, aside?, description?, badge? } as a radio group with a gliding ring
StepperReview{ rows, onEdit? }A summary { label, value, step? } – rows with a step get an Edit link (pass goTo)

Keyboard

  • ↵: Continues (in a chip field: adds the typed entry first, Continue when it's empty)
  • Tab: Moves through the fields, then Back and Continue
  • ← / →: In the plan cards: moves and selects; ↵ picks and continues
  • , / ⌫: In a chip field: comma adds, Backspace in the empty field removes the last chip

Accessibility

  • The steps are an ordered list with aria-current="step"; finished steps are buttons that jump back
  • On narrow screens the list stays for screen readers while the compact "Step 2 of 4" text shows on screen
  • The new step's first field (or its heading) gets the focus; after a failed Continue the first invalid field does
  • Fields carry aria-invalid and point to their message with aria-describedby; "Step 2 of 4: Team" and errors are announced in a polite live region
  • Continue looks dimmed while the step isn't valid but still answers, so keyboard and screen reader users hear what's missing

Responsive

  • Fills its container up to 520px and works down to 320px
  • Below 420px the steps collapse to "Step 2 of 4 · Team", plan cards stack and review rows wrap
  • Touch screens get 16px fields (no zoom on iOS) and buttons of at least 40px; the footer sticks to the bottom in long pages
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Setup Guide – Onboarding panel with a progress bar and collapsible task groups that strike through when done
  • Progress Tracker – Onboarding checklist with an animated progress bar, ticks that draw themselves and auto play
  • Combobox – Pick one or many from a searchable, grouped list – chips with avatars, highlighted matches, create-on-the-fly and full keyboard support
  • Segmented Control – Switch between two to six options – a raised pill or underline that glides, stretches a little and can be dragged across, with icons, counts and badges