--- name: form-wizard-builder description: Builds multi-step forms with validation schemas (Zod/Yup), step components, shared state management, progress indicators, review steps, and error handling. Use when creating "multi-step forms", "wizard flows", "onboarding forms", or "checkout processes". --- # Form Wizard Builder Create multi-step form experiences with validation, state persistence, and review steps. ## Core Workflow 1. **Define steps**: Break form into logical sections 2. **Create schema**: Zod/Yup validation for each step 3. **Build step components**: Individual form sections 4. **State management**: Shared state across steps (Zustand/Context) 5. **Navigation**: Next/Back/Skip logic 6. **Progress indicator**: Visual step tracker 7. **Review step**: Summary before submission 8. **Error handling**: Per-step and final validation ## Basic Wizard Structure ```typescript // types/wizard.ts export type WizardStep = { id: string; title: string; description?: string; component: React.ComponentType; schema: z.ZodSchema; isOptional?: boolean; }; export type WizardData = { personal: PersonalInfoData; contact: ContactData; preferences: PreferencesData; }; ``` ## Validation Schemas (Zod) ```typescript // schemas/wizard.schema.ts import { z } from "zod"; export const personalInfoSchema = z.object({ firstName: z.string().min(2, "First name must be at least 2 characters"), lastName: z.string().min(2, "Last name must be at least 2 characters"), dateOfBirth: z.string().refine((date) => { const age = new Date().getFullYear() - new Date(date).getFullYear(); return age >= 18; }, "Must be at least 18 years old"), }); export const contactSchema = z.object({ email: z.string().email("Invalid email address"), phone: z.string().regex(/^\+?[\d\s-()]+$/, "Invalid phone number"), address: z.object({ street: z.string().min(1, "Street is required"), city: z.string().min(1, "City is required"), zipCode: z.string().regex(/^\d{5}(-\d{4})?$/, "Invalid ZIP code"), }), }); export const preferencesSchema = z.object({ notifications: z.object({ email: z.boolean(), sms: z.boolean(), push: z.boolean(), }), interests: z.array(z.string()).min(1, "Select at least one interest"), }); // Complete wizard schema export const wizardSchema = z.object({ personal: personalInfoSchema, contact: contactSchema, preferences: preferencesSchema, }); export type WizardFormData = z.infer; ``` ## State Management (Zustand) ```typescript // stores/wizard.store.ts import { create } from "zustand"; import { persist } from "zustand/middleware"; interface WizardState { currentStep: number; data: Partial; completedSteps: number[]; isSubmitting: boolean; setCurrentStep: (step: number) => void; updateStepData: (step: string, data: any) => void; markStepComplete: (step: number) => void; nextStep: () => void; prevStep: () => void; resetWizard: () => void; submitWizard: () => Promise; } export const useWizardStore = create()( persist( (set, get) => ({ currentStep: 0, data: {}, completedSteps: [], isSubmitting: false, setCurrentStep: (step) => set({ currentStep: step }), updateStepData: (step, newData) => set((state) => ({ data: { ...state.data, [step]: { ...state.data[step], ...newData }, }, })), markStepComplete: (step) => set((state) => ({ completedSteps: Array.from(new Set([...state.completedSteps, step])), })), nextStep: () => set((state) => ({ currentStep: Math.min(state.currentStep + 1, steps.length - 1), })), prevStep: () => set((state) => ({ currentStep: Math.max(state.currentStep - 1, 0), })), resetWizard: () => set({ currentStep: 0, data: {}, completedSteps: [], isSubmitting: false, }), submitWizard: async () => { set({ isSubmitting: true }); try { // Submit to API await fetch("/api/wizard", { method: "POST", body: JSON.stringify(get().data), }); get().resetWizard(); } catch (error) { console.error("Submission failed:", error); } finally { set({ isSubmitting: false }); } }, }), { name: "wizard-storage", } ) ); ``` ## Main Wizard Component ```typescript // components/Wizard.tsx "use client"; import { useState } from "react"; import { useWizardStore } from "@/stores/wizard.store"; import { ProgressIndicator } from "./ProgressIndicator"; import { PersonalInfoStep } from "./steps/PersonalInfoStep"; import { ContactStep } from "./steps/ContactStep"; import { PreferencesStep } from "./steps/PreferencesStep"; import { ReviewStep } from "./steps/ReviewStep"; const steps = [ { id: "personal", title: "Personal Information", component: PersonalInfoStep, schema: personalInfoSchema, }, { id: "contact", title: "Contact Details", component: ContactStep, schema: contactSchema, }, { id: "preferences", title: "Preferences", component: PreferencesStep, schema: preferencesSchema, isOptional: true, }, { id: "review", title: "Review", component: ReviewStep, schema: z.any(), }, ]; export function Wizard() { const { currentStep } = useWizardStore(); const CurrentStepComponent = steps[currentStep].component; return (

{steps[currentStep].title}

{steps[currentStep].description && (

{steps[currentStep].description}

)}
); } ``` ## Progress Indicator ```typescript // components/ProgressIndicator.tsx import { cn } from "@/lib/utils"; import { CheckIcon } from "@/components/icons"; interface ProgressIndicatorProps { steps: Array<{ id: string; title: string }>; currentStep: number; } export function ProgressIndicator({ steps, currentStep, }: ProgressIndicatorProps) { return ( ); } ``` ## Step Component Example ```typescript // components/steps/PersonalInfoStep.tsx "use client"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useWizardStore } from "@/stores/wizard.store"; import { personalInfoSchema } from "@/schemas/wizard.schema"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; export function PersonalInfoStep() { const { data, updateStepData, markStepComplete, nextStep } = useWizardStore(); const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(personalInfoSchema), defaultValues: data.personal || {}, }); const onSubmit = (formData: any) => { updateStepData("personal", formData); markStepComplete(0); nextStep(); }; return (
); } ``` ## Review Step ```typescript // components/steps/ReviewStep.tsx "use client"; import { useWizardStore } from "@/stores/wizard.store"; import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; export function ReviewStep() { const { data, isSubmitting, submitWizard, setCurrentStep } = useWizardStore(); return (

Personal Information

Name:
{data.personal?.firstName} {data.personal?.lastName}
Date of Birth:
{data.personal?.dateOfBirth}

Contact Details

Email:
{data.contact?.email}
Phone:
{data.contact?.phone}
); } ``` ## Navigation Controls ```typescript // components/WizardNavigation.tsx interface WizardNavigationProps { onNext?: () => void; onPrev?: () => void; onSkip?: () => void; isFirstStep: boolean; isLastStep: boolean; isOptional?: boolean; nextLabel?: string; prevLabel?: string; } export function WizardNavigation({ onNext, onPrev, onSkip, isFirstStep, isLastStep, isOptional, nextLabel = "Next", prevLabel = "Back", }: WizardNavigationProps) { return (
{!isFirstStep && ( )}
{isOptional && ( )}
); } ``` ## Persistence (LocalStorage) ```typescript // hooks/useWizardPersistence.ts import { useEffect } from "react"; import { useWizardStore } from "@/stores/wizard.store"; export function useWizardPersistence() { const { data, currentStep } = useWizardStore(); // Auto-save to localStorage useEffect(() => { localStorage.setItem("wizard-data", JSON.stringify(data)); localStorage.setItem("wizard-step", String(currentStep)); }, [data, currentStep]); // Load on mount useEffect(() => { const savedData = localStorage.getItem("wizard-data"); const savedStep = localStorage.getItem("wizard-step"); if (savedData) { // Restore state } }, []); } ``` ## Best Practices 1. **Validate per step**: Don't wait until end 2. **Save progress**: Persist to localStorage/server 3. **Allow navigation**: Let users go back and edit 4. **Show progress**: Clear visual indicator 5. **Review before submit**: Summary step is crucial 6. **Handle errors gracefully**: Show which step has errors 7. **Mobile responsive**: Stack progress on mobile 8. **Accessibility**: Keyboard navigation, ARIA labels ## Output Checklist - [ ] Step definitions with schemas - [ ] Validation with Zod/Yup - [ ] State management (Zustand/Context) - [ ] Progress indicator component - [ ] Individual step components - [ ] Navigation controls (Next/Back/Skip) - [ ] Review/summary step - [ ] Error handling per step - [ ] Persistence mechanism - [ ] Mobile-responsive design