TL;DR: Multi-step forms (wizards) split a long form into digestible steps. TanStack Form's field-level validation, computed fields, and form state make this natural. This guide covers step navigation with validation guards, partial submission per step, state persistence across steps, and a progress indicator that responds to field completion.
Introduction
Long forms hurt conversion. A checkout with 20 fields can see 60% abandonment. The fix: break it into 4-5 steps with a progress indicator. But multi-step forms introduce complexity -- step-level validation, state persistence when navigating back, and partial server submissions.
TanStack Form's granular field API makes this manageable. Each step validates independently, field values persist reactively, and you submit only at the final step.
Wizard Store with TanStack Form
For a comprehensive overview of TanStack Form's API, see the TanStack Form complete guide. The wizard pattern builds on its core form state and field-level validation:
import { useForm } from '@tanstack/react-form'
import { z } from 'zod'
const wizardSchema = z.object({
// Step 1: Account
email: z.string().email(),
password: z.string().min(8),
// Step 2: Profile
fullName: z.string().min(2),
companyName: z.string().min(1),
// Step 3: Preferences
role: z.enum(['developer', 'manager', 'founder']),
teamSize: z.enum(['1', '2-10', '11-50', '50+']),
})
function OnboardingWizard() {
const [step, setStep] = useState(1)
const form = useForm({
defaultValues: { email: '', password: '', fullName: '', companyName: '', role: 'developer', teamSize: '1' },
validators: { onSubmit: wizardSchema },
})
Step Validation
const canProceed = useMemo(() => {
if (step === 1) return form.state.values.email && form.state.values.password.length >= 8
if (step === 2) return form.state.values.fullName && form.state.values.companyName
return true
}, [step, form.state.values])
return (
<div>
<ProgressBar current={step} total={3} />
{step === 1 && <AccountStep form={form} />}
{step === 2 && <ProfileStep form={form} />}
{step === 3 && <PreferencesStep form={form} />}
<div className="flex justify-between mt-6">
{step > 1 && <button onClick={() => setStep(step - 1)}>Back</button>}
{step < 3
? <button disabled={!canProceed} onClick={() => setStep(step + 1)}>Next</button>
: <form.Subscribe selector={(state) => state.isSubmitting}>
{(isSubmitting) => (
<button disabled={isSubmitting} onClick={() => form.handleSubmit()}>
{isSubmitting ? 'Submitting...' : 'Complete Setup'}
</button>
)}
</form.Subscribe>
}
</div>
</div>
)
}
Wizard Pattern Decision Guide
Choosing the right form pattern depends on your use case. For deeper insights into form design UX patterns, see our dedicated guide.
| Pattern | When to Use | UX Benefit |
|---|---|---|
| Single step, long form | <10 fields, familiar data | Fast completion |
| Multi-step wizard | 10-30 fields, unfamiliar | Reduced overwhelm |
| Conditional branching | User persona affects fields | Relevance |
| Auto-save per step | Critical data, long sessions | No data loss |
| Side-by-side preview | Configuration-heavy | Immediate feedback |
Conclusion
TanStack Form's per-field validation and reactive state make multi-step wizards straightforward. The pattern reduces form abandonment for complex onboarding, checkout, and survey flows. For a complete onboarding strategy, see SaaS Customer Onboarding Best Practices and Signup Flow Optimization.