TanStack FormWizardTypeScriptReact

TanStack Form: Multi-Step Wizard Pattern

Build complex multi-step forms and wizards with TanStack Form, covering step navigation, validation per step, persisted state, and progress tracking.

Sam Rivera
Sam Rivera
June 12, 202611 min read

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:

tsx
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

tsx
  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.

PatternWhen to UseUX Benefit
Single step, long form<10 fields, familiar dataFast completion
Multi-step wizard10-30 fields, unfamiliarReduced overwhelm
Conditional branchingUser persona affects fieldsRelevance
Auto-save per stepCritical data, long sessionsNo data loss
Side-by-side previewConfiguration-heavyImmediate 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.