home / skills / amnadtaowsoam / cerebraskills / multi-step-forms
This skill helps you design robust multi-step forms with wizard patterns, per-step validation, and auto-save to boost completion rates and UX.
npx playbooks add skill amnadtaowsoam/cerebraskills --skill multi-step-formsReview the files below or copy the command above to add this skill to your agents.
---
name: Multi-Step Form Patterns
description: Expert-level framework for building complex forms using wizard patterns, progressive disclosure, validation, and persistence strategies to improve completion rates and user experience.
---
# Multi-Step Form Patterns
## Overview
Multi-Step Forms break complex data collection into manageable chunks using wizard patterns. This approach reduces cognitive load, improves completion rates by 20-30%, and provides better user experience through progress indicators, per-step validation, and auto-save functionality. Using React Hook Form for state management and Zod for validation ensures robust form handling with TypeScript support.
## Why This Matters
- **Increases Completion Rate**: Breaking forms into steps improves completion rates by 20-30%
- **Reduces Form Abandonment**: Progressive disclosure reduces abandonment rate
- **Improves Data Quality**: Per-step validation increases data accuracy
- **Enhances User Experience**: Progress indicators and auto-save improve UX
- **Boosts Conversion**: Easier form completion increases conversion rate
---
## Core Concepts
### 1. Form Architecture
Multi-step form structure:
- **Form Container**: Manages overall form state and orchestration
- **Step Navigator**: Controls step transitions and navigation
- **Progress Indicator**: Visual feedback showing completion status
- **Step Components**: Individual form step components
- **Validation Engine**: Per-step and global validation
- **State Persistence**: Auto-save and form state management
### 2. Validation Strategy
Comprehensive validation approach:
- **Per-Step Validation**: Validate current step before proceeding
- **Global Validation**: Validate all steps on final submission
- **Async Validation**: Server-side validation for complex checks
- **Error Display**: Clear error messages at field and step level
- **Schema Validation**: Using Zod for type-safe validation
### 3. State Management
Form state handling:
- **Form Data**: Centralized state for all form fields
- **Step State**: Current step, completed steps, navigation history
- **Error State**: Validation errors organized by step and field
- **Submitting State**: Loading state for submission
- **Draft State**: Auto-saved draft data
### 4. Progress Indicators
Visual progress feedback:
- **Linear Progress**: Progress bar showing completion percentage
- **Step Indicators**: Visual step markers with status (active, completed, pending)
- **Circular Progress**: Circular progress indicator for overall completion
- **Milestones**: Checkpoints highlighting key achievements
### 5. Persistence Strategy
Data preservation:
- **Local Storage**: Client-side persistence for drafts
- **Session Storage**: Temporary persistence during session
- **Server Storage**: Server-side draft storage
- **Auto-Save**: Debounced automatic saving
- **Manual Save**: Explicit save buttons
## Quick Start
1. **Setup Form Hook**: Create useMultiStepForm hook for state management
2. **Define Schemas**: Create Zod schemas for each step
3. **Build Steps**: Create individual step components
4. **Add Progress**: Implement progress indicators
5. **Implement Validation**: Add per-step and global validation
6. **Add Persistence**: Implement auto-save functionality
7. **Handle Navigation**: Implement step navigation with validation
8. **Test Accessibility**: Verify keyboard navigation and screen reader support
```typescript
// useMultiStepForm Hook
'use client'
import { useState, useCallback } from 'react'
interface UseMultiStepFormOptions<T> {
initialData: T
steps: string[]
onSubmit: (data: T) => Promise<void>
validate?: (step: number, data: T) => Promise<ValidationErrors>
onStepChange?: (step: number) => void
}
interface ValidationErrors {
[key: string]: string
}
export function useMultiStepForm<T extends Record<string, any>>(
options: UseMultiStepFormOptions<T>
) {
const [currentStep, setCurrentStep] = useState(0)
const [formData, setFormData] = useState<T>(options.initialData)
const [errors, setErrors] = useState<ValidationErrors>({})
const [isSubmitting, setIsSubmitting] = useState(false)
const [completedSteps, setCompletedSteps] = useState<Set<number>>(new Set())
const totalSteps = options.steps.length
const updateFormData = useCallback((updates: Partial<T>) => {
setFormData((prev) => ({ ...prev, ...updates }))
}, [])
const validateCurrentStep = useCallback(async (): Promise<boolean> => {
if (!options.validate) return true
const stepErrors = await options.validate(currentStep, formData)
setErrors(stepErrors)
return Object.keys(stepErrors).length === 0
}, [currentStep, formData, options])
const goToStep = useCallback(
async (step: number) => {
if (step < 0 || step >= totalSteps) return
// Validate current step before moving forward
if (step > currentStep) {
const isValid = await validateCurrentStep()
if (!isValid) return
}
setCurrentStep(step)
options.onStepChange?.(step)
// Mark previous step as completed
if (step > currentStep) {
setCompletedSteps((prev) => new Set([...prev, currentStep]))
}
},
[currentStep, totalSteps, validateCurrentStep, options]
)
const nextStep = useCallback(async () => {
await goToStep(currentStep + 1)
}, [currentStep, goToStep])
const previousStep = useCallback(() => {
goToStep(currentStep - 1)
}, [currentStep, goToStep])
const handleSubmit = useCallback(async () => {
// Validate all steps
const isValid = await validateCurrentStep()
if (!isValid) return
setIsSubmitting(true)
try {
await options.onSubmit(formData)
} catch (error) {
console.error('Form submission failed:', error)
throw error
} finally {
setIsSubmitting(false)
}
}, [formData, validateCurrentStep, options])
const resetForm = useCallback(() => {
setCurrentStep(0)
setFormData(options.initialData)
setErrors({})
setCompletedSteps(new Set())
}, [options.initialData])
return {
currentStep,
totalSteps,
formData,
errors,
isSubmitting,
completedSteps,
updateFormData,
nextStep,
previousStep,
goToStep,
handleSubmit,
resetForm,
isFirstStep: currentStep === 0,
isLastStep: currentStep === totalSteps - 1,
progress: ((currentStep + 1) / totalSteps) * 100,
}
}
```
## Production Checklist
- [ ] Form state management hook implemented
- [ ] Validation schemas defined for each step
- [ ] Progress indicators implemented and visible
- [ ] Per-step validation working correctly
- [ ] Global validation on final submission
- [ ] Auto-save functionality implemented
- [ ] Form persistence (localStorage/server storage)
- [ ] Navigation between steps with validation
- [ ] Loading states for async operations
- [ ] Error display at field and step level
- [ ] Keyboard navigation working
- [ ] Screen reader support verified
- [ ] Mobile responsiveness tested
- [ ] Form tested on all browsers
- [ ] Performance optimized (lazy loading, debouncing)
## Anti-patterns
1. **Lost Data**: Not implementing persistence causes data loss on refresh
2. **Confusing Navigation**: Unclear progress indicators frustrate users
3. **Validation Timing**: Validating at wrong times causes poor UX
4. **Performance Issues**: Not optimizing re-renders causes sluggish interface
5. **Accessibility Problems**: Missing keyboard navigation and screen reader support
6. **Poor UX**: Steps that are too long or complex overwhelm users
7. **No Back Navigation**: Preventing users from going back creates frustration
8. **Poor Error Messages**: Unclear error messages don't help users fix issues
9. **Skipping Validation**: Not validating properly allows invalid data
10. **Complex State**: Overly complex state management makes maintenance difficult
## Integration Points
- **React Hook Form**: Form state management and validation
- **Zod**: Schema validation and type safety
- **React**: Core React for components and hooks
- **TypeScript**: Type safety and interfaces
- **Lodash**: Utility functions (debounce, throttle)
- [`form-handling`](../form-handling/SKILL.md) for general form patterns
- [`react-best-practices`](../react-best-practices/SKILL.md) for React patterns
- [`state-management`](../../05-state-management/SKILL.md) for state management options
- [`accessibility`](../../22-ux-ui-design/accessibility/SKILL.md) for accessibility guidelines
## Further Reading
- [React Hook Form Documentation](https://react-hook-form.com/) - Form management library
- [Zod Documentation](https://zod.dev/) - TypeScript-first schema validation
- [Multi-Step Form UX Best Practices](https://www.nngroup.com/articles/multi-page-forms/) - UX research
- [Form Validation Patterns](https://www.smashingmagazine.com/2020/02/form-validation-patterns/) - Validation patterns
- [Accessibility for Forms](https://www.w3.org/WAI/tutorials/forms/) - W3C accessibility tutorial
- [Progressive Disclosure](https://lawsofux.com/progressive-disclosure/) - UX principle
This skill provides an expert-level framework for building complex, multi-step forms (wizards) that increase completion rates and improve user experience. It focuses on modular architecture, per-step and global validation, progress indicators, and persistence strategies like auto-save and server drafts. The patterns are implementation-agnostic but map naturally to React + TypeScript stacks using React Hook Form and Zod.
The framework breaks a large form into discrete step components coordinated by a central form container and step navigator. Each step runs its own validation (sync or async) before advancing, while a global validation pass runs on final submission. State is centralized for form data, errors, submission status, and completed steps, with debounced auto-save options for local or server-side persistence. Progress indicators and accessibility checks provide clear feedback and keyboard/screen reader support.
How do I handle server-side validation that depends on multiple steps?
Run async validation on the server during the per-step check or on final submit; surface errors tied to the appropriate step and prevent progression until resolved.
When should I use localStorage vs server drafts?
Use localStorage for fast, client-only draft persistence and offline resilience; use server drafts for cross-device continuity, longer retention, and shared sessions.
How do I keep performance under control with many fields?
Lazy-load step components, memoize field groups, debounce auto-save, and minimize re-renders by scoping state updates to affected fields.