Multi-step form (wizard) — a form with multiple steps, where the user fills in data sequentially.
Why it matters: Long forms overwhelm users and lead to high abandonment rates. Multi-step forms break the process into digestible chunks, showing progress and allowing the user to go back and edit previous steps. They are standard in checkout flows, registration processes, and onboarding wizards.
How it works:
onNext callback.1import { useState } from "react";23// Types4interface Step1Data { name: string; email: string; }5interface Step2Data { address: string; city: string; }6interface FormData extends Step1Data, Step2Data {}78// Step 19function Step1({ data, onNext }) {10 const [name, setName] = useState(data.name);11 const [email, setEmail] = useState(data.email);1213 return (14 <div>15 <h2>Personal Info</h2>16 <input value={name} onChange={e => setName(e.target.value)} placeholder="Name" />17 <input value={email} onChange={e => setEmail(e.target.value)} placeholder="Email" />18 <button onClick={() => onNext({ name, email })}>Next</button>19 </div>20 );21}2223// Step 224function Step2({ data, onBack, onSubmit }) {25 const [address, setAddress] = useState(data.address);26 const [city, setCity] = useState(data.city);2728 return (29 <div>30 <h2>Address</h2>31 <input value={address} onChange={e => setAddress(e.target.value)} placeholder="Address" />32 <input value={city} onChange={e => setCity(e.target.value)} placeholder="City" />33 <button onClick={onBack}>Back</button>34 <button onClick={() => onSubmit({ address, city })}>Submit</button>35 </div>36 );37}3839// Wizard40function Wizard() {41 const [step, setStep] = useState(1);42 const [data, setData] = useState<Partial<FormData>>({});4344 const handleStep1 = (step1Data: Step1Data) => {45 setData(prev => ({ ...prev, ...step1Data }));46 setStep(2);47 };4849 const handleSubmit = async (step2Data: Step2Data) => {50 const finalData = { ...data, ...step2Data };51 await fetch("/api/register", {52 method: "POST",53 body: JSON.stringify(finalData),54 });55 };5657 return (58 <div>59 <div>Step {step} of 2</div>60 {step === 1 && <Step1 data={data} onNext={handleStep1} />}61 {step === 2 && <Step2 data={data} onBack={() => setStep(1)} onSubmit={handleSubmit} />}62 </div>63 );64}
Advanced pattern — with validation per step:
1function WizardWithValidation() {2 const [step, setStep] = useState(1);3 const [errors, setErrors] = useState({});4 const [data, setData] = useState({});56 const validateAndNext = (stepData, validationSchema) => {7 const result = validationSchema.safeParse(stepData);8 if (!result.success) {9 setErrors(result.error.flatten().fieldErrors);10 return;11 }12 setErrors({});13 setData(prev => ({ ...prev, ...stepData }));14 setStep(s => s + 1);15 };1617 return (18 <div>19 {step === 1 && <Step1 data={data} errors={errors} onNext={(d) => validateAndNext(d, step1Schema)} />}20 {step === 2 && <Step2 data={data} errors={errors} onNext={(d) => validateAndNext(d, step2Schema)} />}21 </div>22 );23}
Common pitfalls:
Libraries: