home / skills / amnadtaowsoam / cerebraskills / multi-step-forms

multi-step-forms skill

/02-frontend/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-forms

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
8.9 KB
---
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

Overview

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.

How this skill works

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.

When to use it

  • Long or complex data collection flows (applications, onboarding, multi-page surveys)
  • When reducing cognitive load will improve completion or conversion rates
  • When data needs incremental validation or server-side checks during entry
  • When draft persistence or interrupted sessions must be supported
  • When measurable UX improvements (progress, milestones) are required

Best practices

  • Validate each step before allowing forward navigation and run a global validation on final submit
  • Persist drafts with debounced auto-save to localStorage and optional server-side sync
  • Keep steps short and focused; group related fields and surface only necessary inputs (progressive disclosure)
  • Provide clear, contextual error messages at field and step level and enable back navigation
  • Optimize renders (memoize step components, debounce saves) and test keyboard/screen reader workflows

Example use cases

  • Multi-page application forms with saved drafts and server-side identity checks
  • Onboarding wizards that progressively request permissions and profile details
  • Complex checkout flows splitting shipping, billing, and extras into steps with per-step pricing validation
  • Enterprise data entry tools where users may pause and resume via server-stored drafts
  • Surveys that show progress and unlock subsequent sections based on previous answers

FAQ

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.