Claude-skill-registry form-wizard-builder
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".
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/form-wizard-builder" ~/.claude/skills/majiayu000-claude-skill-registry-form-wizard-builder && rm -rf "$T"
manifest:
skills/data/form-wizard-builder/SKILL.mdsource content
Form Wizard Builder
Create multi-step form experiences with validation, state persistence, and review steps.
Core Workflow
- Define steps: Break form into logical sections
- Create schema: Zod/Yup validation for each step
- Build step components: Individual form sections
- State management: Shared state across steps (Zustand/Context)
- Navigation: Next/Back/Skip logic
- Progress indicator: Visual step tracker
- Review step: Summary before submission
- Error handling: Per-step and final validation
Basic Wizard Structure
// types/wizard.ts export type WizardStep = { id: string; title: string; description?: string; component: React.ComponentType<StepProps>; schema: z.ZodSchema; isOptional?: boolean; }; export type WizardData = { personal: PersonalInfoData; contact: ContactData; preferences: PreferencesData; };
Validation Schemas (Zod)
// 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<typeof wizardSchema>;
State Management (Zustand)
// stores/wizard.store.ts import { create } from "zustand"; import { persist } from "zustand/middleware"; interface WizardState { currentStep: number; data: Partial<WizardFormData>; 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<void>; } export const useWizardStore = create<WizardState>()( 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
// 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 ( <div className="mx-auto max-w-2xl space-y-8 p-6"> <ProgressIndicator steps={steps} currentStep={currentStep} /> <div className="rounded-lg border bg-white p-8 shadow-sm"> <div className="mb-6"> <h2 className="text-2xl font-bold">{steps[currentStep].title}</h2> {steps[currentStep].description && ( <p className="text-gray-600">{steps[currentStep].description}</p> )} </div> <CurrentStepComponent /> </div> </div> ); }
Progress Indicator
// 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 ( <nav aria-label="Progress"> <ol className="flex items-center justify-between"> {steps.map((step, index) => { const isComplete = index < currentStep; const isCurrent = index === currentStep; return ( <li key={step.id} className="flex flex-1 items-center"> <div className="flex flex-col items-center"> <div className={cn( "flex h-10 w-10 items-center justify-center rounded-full border-2", isComplete && "border-primary-500 bg-primary-500", isCurrent && "border-primary-500 bg-white", !isComplete && !isCurrent && "border-gray-300 bg-white" )} > {isComplete ? ( <CheckIcon className="h-5 w-5 text-white" /> ) : ( <span className={cn( "text-sm font-medium", isCurrent ? "text-primary-500" : "text-gray-500" )} > {index + 1} </span> )} </div> <span className={cn( "mt-2 text-sm font-medium", isCurrent ? "text-primary-500" : "text-gray-500" )} > {step.title} </span> </div> {index < steps.length - 1 && ( <div className={cn( "mx-4 h-0.5 flex-1", isComplete ? "bg-primary-500" : "bg-gray-300" )} /> )} </li> ); })} </ol> </nav> ); }
Step Component Example
// 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 ( <form onSubmit={handleSubmit(onSubmit)} className="space-y-6"> <div className="space-y-4"> <div className="grid gap-4 sm:grid-cols-2"> <div className="space-y-2"> <Label htmlFor="firstName">First Name</Label> <Input id="firstName" {...register("firstName")} error={errors.firstName?.message} /> </div> <div className="space-y-2"> <Label htmlFor="lastName">Last Name</Label> <Input id="lastName" {...register("lastName")} error={errors.lastName?.message} /> </div> </div> <div className="space-y-2"> <Label htmlFor="dateOfBirth">Date of Birth</Label> <Input id="dateOfBirth" type="date" {...register("dateOfBirth")} error={errors.dateOfBirth?.message} /> </div> </div> <div className="flex justify-end"> <Button type="submit">Next Step</Button> </div> </form> ); }
Review Step
// 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 ( <div className="space-y-6"> <Card className="p-6"> <div className="mb-4 flex items-center justify-between"> <h3 className="text-lg font-semibold">Personal Information</h3> <Button variant="ghost" size="sm" onClick={() => setCurrentStep(0)}> Edit </Button> </div> <dl className="space-y-2"> <div className="flex justify-between"> <dt className="text-gray-600">Name:</dt> <dd className="font-medium"> {data.personal?.firstName} {data.personal?.lastName} </dd> </div> <div className="flex justify-between"> <dt className="text-gray-600">Date of Birth:</dt> <dd className="font-medium">{data.personal?.dateOfBirth}</dd> </div> </dl> </Card> <Card className="p-6"> <div className="mb-4 flex items-center justify-between"> <h3 className="text-lg font-semibold">Contact Details</h3> <Button variant="ghost" size="sm" onClick={() => setCurrentStep(1)}> Edit </Button> </div> <dl className="space-y-2"> <div className="flex justify-between"> <dt className="text-gray-600">Email:</dt> <dd className="font-medium">{data.contact?.email}</dd> </div> <div className="flex justify-between"> <dt className="text-gray-600">Phone:</dt> <dd className="font-medium">{data.contact?.phone}</dd> </div> </dl> </Card> <div className="flex justify-between"> <Button variant="outline" onClick={() => setCurrentStep((prev) => prev - 1)} > Back </Button> <Button onClick={submitWizard} isLoading={isSubmitting}> Submit Application </Button> </div> </div> ); }
Navigation Controls
// 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 ( <div className="flex items-center justify-between"> <div> {!isFirstStep && ( <Button variant="outline" onClick={onPrev}> {prevLabel} </Button> )} </div> <div className="flex gap-2"> {isOptional && ( <Button variant="ghost" onClick={onSkip}> Skip </Button> )} <Button onClick={onNext}>{isLastStep ? "Submit" : nextLabel}</Button> </div> </div> ); }
Persistence (LocalStorage)
// 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
- Validate per step: Don't wait until end
- Save progress: Persist to localStorage/server
- Allow navigation: Let users go back and edit
- Show progress: Clear visual indicator
- Review before submit: Summary step is crucial
- Handle errors gracefully: Show which step has errors
- Mobile responsive: Stack progress on mobile
- 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