form-wizard-builder

Create multi-step form experiences with validation, state persistence, and review steps.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "form-wizard-builder" with this command: npx skills add monkey1sai/openai-cli/monkey1sai-openai-cli-form-wizard-builder

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} />

  &#x3C;div className="rounded-lg border bg-white p-8 shadow-sm">
    &#x3C;div className="mb-6">
      &#x3C;h2 className="text-2xl font-bold">{steps[currentStep].title}&#x3C;/h2>
      {steps[currentStep].description &#x26;&#x26; (
        &#x3C;p className="text-gray-600">{steps[currentStep].description}&#x3C;/p>
      )}
    &#x3C;/div>

    &#x3C;CurrentStepComponent />
  &#x3C;/div>
&#x3C;/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 (
        &#x3C;li key={step.id} className="flex flex-1 items-center">
          &#x3C;div className="flex flex-col items-center">
            &#x3C;div
              className={cn(
                "flex h-10 w-10 items-center justify-center rounded-full border-2",
                isComplete &#x26;&#x26; "border-primary-500 bg-primary-500",
                isCurrent &#x26;&#x26; "border-primary-500 bg-white",
                !isComplete &#x26;&#x26; !isCurrent &#x26;&#x26; "border-gray-300 bg-white"
              )}
            >
              {isComplete ? (
                &#x3C;CheckIcon className="h-5 w-5 text-white" />
              ) : (
                &#x3C;span
                  className={cn(
                    "text-sm font-medium",
                    isCurrent ? "text-primary-500" : "text-gray-500"
                  )}
                >
                  {index + 1}
                &#x3C;/span>
              )}
            &#x3C;/div>
            &#x3C;span
              className={cn(
                "mt-2 text-sm font-medium",
                isCurrent ? "text-primary-500" : "text-gray-500"
              )}
            >
              {step.title}
            &#x3C;/span>
          &#x3C;/div>

          {index &#x3C; steps.length - 1 &#x26;&#x26; (
            &#x3C;div
              className={cn(
                "mx-4 h-0.5 flex-1",
                isComplete ? "bg-primary-500" : "bg-gray-300"
              )}
            />
          )}
        &#x3C;/li>
      );
    })}
  &#x3C;/ol>
&#x3C;/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>

      &#x3C;div className="space-y-2">
        &#x3C;Label htmlFor="lastName">Last Name&#x3C;/Label>
        &#x3C;Input
          id="lastName"
          {...register("lastName")}
          error={errors.lastName?.message}
        />
      &#x3C;/div>
    &#x3C;/div>

    &#x3C;div className="space-y-2">
      &#x3C;Label htmlFor="dateOfBirth">Date of Birth&#x3C;/Label>
      &#x3C;Input
        id="dateOfBirth"
        type="date"
        {...register("dateOfBirth")}
        error={errors.dateOfBirth?.message}
      />
    &#x3C;/div>
  &#x3C;/div>

  &#x3C;div className="flex justify-end">
    &#x3C;Button type="submit">Next Step&#x3C;/Button>
  &#x3C;/div>
&#x3C;/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>

  &#x3C;Card className="p-6">
    &#x3C;div className="mb-4 flex items-center justify-between">
      &#x3C;h3 className="text-lg font-semibold">Contact Details&#x3C;/h3>
      &#x3C;Button variant="ghost" size="sm" onClick={() => setCurrentStep(1)}>
        Edit
      &#x3C;/Button>
    &#x3C;/div>
    &#x3C;dl className="space-y-2">
      &#x3C;div className="flex justify-between">
        &#x3C;dt className="text-gray-600">Email:&#x3C;/dt>
        &#x3C;dd className="font-medium">{data.contact?.email}&#x3C;/dd>
      &#x3C;/div>
      &#x3C;div className="flex justify-between">
        &#x3C;dt className="text-gray-600">Phone:&#x3C;/dt>
        &#x3C;dd className="font-medium">{data.contact?.phone}&#x3C;/dd>
      &#x3C;/div>
    &#x3C;/dl>
  &#x3C;/Card>

  &#x3C;div className="flex justify-between">
    &#x3C;Button
      variant="outline"
      onClick={() => setCurrentStep((prev) => prev - 1)}
    >
      Back
    &#x3C;/Button>
    &#x3C;Button onClick={submitWizard} isLoading={isSubmitting}>
      Submit Application
    &#x3C;/Button>
  &#x3C;/div>
&#x3C;/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>

  &#x3C;div className="flex gap-2">
    {isOptional &#x26;&#x26; (
      &#x3C;Button variant="ghost" onClick={onSkip}>
        Skip
      &#x3C;/Button>
    )}
    &#x3C;Button onClick={onNext}>{isLastStep ? "Submit" : nextLabel}&#x3C;/Button>
  &#x3C;/div>
&#x3C;/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

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

responsive-design-system

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

rate-limiting-abuse-protection

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

bruno-collection-generator

No summary provided by upstream source.

Repository SourceNeeds Review