Aesthe UI

Multi-Step Form

Beautiful, configurable multi-step form components with validation and progress tracking

Overview

A powerful, fully configurable multi-step form component with built-in validation, progress tracking, and beautiful UI. Perfect for registration flows, surveys, checkout processes, and any multi-step data collection needs.

Features

  • Fully Configurable - Define steps and fields via configuration objects
  • Built-in Validation - Powered by Zod and React Hook Form
  • Multiple Field Types - Text, email, number, tel, date, textarea, and select
  • Step Indicator - Beautiful progress visualization with animations
  • Responsive Design - Works seamlessly on all screen sizes
  • Success Dialog - Customizable completion confirmation
  • Type Safe - Full TypeScript support
  • Dark Mode - Fully supports dark/light themes

Variants

Multi-Step Form - Glass Morphism

A stunning form with glass morphism effects and liquid-style background.

Complete Your Registration
Follow the steps below to get started
1

Step 1

Getting started

2

Step 2

In progress

3

Step 3

Almost there

4

Step 4

Complete


Configuration

Step Configuration

Define the steps for your form:

interface Step {
  id: number
  name: string
  description: string
}

const steps: Step[] = [
  { id: 1, name: "Personal", description: "Basic info" },
  { id: 2, name: "Contact", description: "Contact details" },
  { id: 3, name: "Review", description: "Confirm" },
]

Field Configuration

Configure fields with full validation support:

type FieldType = "text" | "email" | "number" | "tel" | "date" | "textarea" | "select"

interface FieldConfig {
  name: string
  label: string
  type: FieldType
  placeholder?: string
  gridColumn?: "full" | "half" // Layout control
  validation?: {
    required?: string
    minLength?: { value: number; message: string }
    maxLength?: { value: number; message: string }
    pattern?: { value: RegExp; message: string }
    min?: { value: number; message: string }
    max?: { value: number; message: string }
  }
  options?: { label: string; value: string }[] // For select fields
}

Example Field Configurations

{
  name: "fullName",
  label: "Full Name",
  type: "text",
  placeholder: "John Doe",
  gridColumn: "full", // or "half"
  validation: {
    required: "Name is required",
    minLength: { value: 2, message: "At least 2 characters" },
    maxLength: { value: 50, message: "Max 50 characters" },
  },
}

Component Props

MultiStepFormSimple Props

interface MultiStepFormProps {
  steps: Step[]                              // Array of step configurations
  formData: StepFormData[]                   // Array of field configurations per step
  title?: string                             // Form title (default: "Complete Your Registration")
  description?: string                       // Form description
  onComplete?: (data: Record<string, Record<string, FormFieldValue>>) => void  // Callback on completion
  successTitle?: string                      // Success dialog title
  successDescription?: string                // Success dialog description
}

Complete Example

Here's a comprehensive example with multiple steps and field types:

// lib/form.config.ts
export const registrationSteps = [
  { id: 1, name: "Personal", description: "Basic info" },
  { id: 2, name: "Contact", description: "Details" },
  { id: 3, name: "Profile", description: "About you" },
  { id: 4, name: "Payment", description: "Billing" },
]

export const registrationFields = [
  {
    stepId: 1,
    fields: [
      {
        name: "fullName",
        label: "Full Name",
        type: "text" as const,
        placeholder: "John Doe",
        gridColumn: "full" as const,
        validation: {
          required: "Name is required",
          minLength: { value: 2, message: "Name must be at least 2 characters" },
        },
      },
      {
        name: "email",
        label: "Email Address",
        type: "email" as const,
        placeholder: "john@example.com",
        gridColumn: "half" as const,
        validation: {
          required: "Email is required",
        },
      },
      {
        name: "dateOfBirth",
        label: "Date of Birth",
        type: "date" as const,
        gridColumn: "half" as const,
        validation: {
          required: "Date of birth is required",
        },
      },
    ],
  },
  {
    stepId: 2,
    fields: [
      {
        name: "address",
        label: "Address",
        type: "text" as const,
        placeholder: "123 Main Street",
        gridColumn: "full" as const,
        validation: {
          required: "Address is required",
        },
      },
      {
        name: "city",
        label: "City",
        type: "text" as const,
        placeholder: "New York",
        gridColumn: "half" as const,
        validation: {
          required: "City is required",
        },
      },
      {
        name: "zipCode",
        label: "Zip Code",
        type: "text" as const,
        placeholder: "10001",
        gridColumn: "half" as const,
        validation: {
          required: "Zip code is required",
        },
      },
    ],
  },
  {
    stepId: 3,
    fields: [
      {
        name: "country",
        label: "Country",
        type: "select" as const,
        placeholder: "Select your country",
        gridColumn: "full" as const,
        options: [
          { label: "United States", value: "us" },
          { label: "Canada", value: "ca" },
          { label: "United Kingdom", value: "uk" },
        ],
        validation: {
          required: "Please select a country",
        },
      },
      {
        name: "bio",
        label: "Bio",
        type: "textarea" as const,
        placeholder: "Tell us about yourself...",
        gridColumn: "full" as const,
        validation: {
          minLength: { value: 10, message: "Bio must be at least 10 characters" },
        },
      },
    ],
  },
  {
    stepId: 4,
    fields: [
      {
        name: "cardNumber",
        label: "Card Number",
        type: "text" as const,
        placeholder: "1234 5678 9012 3456",
        gridColumn: "full" as const,
        validation: {
          required: "Card number is required",
          minLength: { value: 16, message: "Card number must be 16 digits" },
        },
      },
      {
        name: "expiryDate",
        label: "Expiry Date",
        type: "text" as const,
        placeholder: "MM/YY",
        gridColumn: "half" as const,
        validation: {
          required: "Expiry date is required",
          pattern: { value: /^(0[1-9]|1[0-2])\/\d{2}$/, message: "Format: MM/YY" },
        },
      },
      {
        name: "cvv",
        label: "CVV",
        type: "text" as const,
        placeholder: "123",
        gridColumn: "half" as const,
        validation: {
          required: "CVV is required",
          minLength: { value: 3, message: "CVV must be 3 digits" },
        },
      },
    ],
  },
]
// app/registration/page.tsx
"use client"

import { MultiStepFormSimple } from '@/components/aesthe-ui/multi-form/multi-step-form'
import { registrationSteps, registrationFields } from '@/lib/form.config'

export default function RegistrationPage() {
  const handleComplete = async (data: Record<string, Record<string, unknown>>) => {
    console.log("Form completed with data:", data)
    
    // Example: Send to API
    try {
      const response = await fetch('/api/register', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(data),
      })
      
      if (response.ok) {
        console.log("Registration successful!")
      }
    } catch (error) {
      console.error("Registration failed:", error)
    }
  }

  return (
    <MultiStepFormSimple
      steps={registrationSteps}
      formData={registrationFields}
      title="Complete Your Registration"
      description="Follow the steps below to create your account"
      onComplete={handleComplete}
      successTitle="Welcome Aboard! 🎉"
      successDescription="Your account has been successfully created. Check your email for verification."
    />
  )
}

Installation

Install Required Dependencies

npm install react-hook-form @hookform/resolvers zod date-fns lucide-react

Copy Component Files

  1. Copy the main component:

    • components/aesthe-ui/multi-form/multi-step-form.tsx
    • components/aesthe-ui/multi-form/multi-step-form-simple.tsx (alternative style)
  2. Copy the step indicator:

    • components/aesthe-ui/multi-form/step-indicator.tsx
  3. Ensure you have the required shadcn/ui components:

    • Button, Card, Dialog, Form, Input, Calendar, Popover, Select, Textarea

Install shadcn/ui Components

npx shadcn-ui@latest add button card dialog form input calendar popover select textarea

Complete Component Code

Multi-Step Form (Glass Morphism)

Copy this complete code to components/aesthe-ui/multi-form/multi-step-form.tsx:

"use client"

import { useState } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { format } from "date-fns"
import { CalendarIcon, Check, ArrowLeft } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Calendar } from "@/components/ui/calendar"
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"
import { Textarea } from "@/components/ui/textarea"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { cn } from "@/lib/utils"
import { StepIndicator } from "./step-indicator"
import BtnShimmer from "@/components/aesthe-ui/button/btn-shimmer"

// Types for configuration
export type FormFieldValue = string | number | Date | boolean | undefined

export interface Step {
  id: number
  name: string
  description: string
}

export type FieldType = "text" | "email" | "number" | "tel" | "date" | "textarea" | "select"

export interface FieldConfig {
  name: string
  label: string
  type: FieldType
  placeholder?: string
  validation?: {
    required?: string
    minLength?: { value: number; message: string }
    maxLength?: { value: number; message: string }
    pattern?: { value: RegExp; message: string }
    min?: { value: number; message: string }
    max?: { value: number; message: string }
  }
  options?: { label: string; value: string }[] // For select fields
  gridColumn?: "full" | "half" // For layout
}

export interface StepFormData {
  stepId: number
  fields: FieldConfig[]
}

export interface MultiStepFormProps {
  steps: Step[]
  formData: StepFormData[]
  title?: string
  description?: string
  onComplete?: (data: Record<string, Record<string, FormFieldValue>>) => void
  successTitle?: string
  successDescription?: string
}

// Helper to create Zod schema from field config
function createZodSchema(fields: FieldConfig[]) {
  const schemaFields: Record<string, z.ZodTypeAny> = {}

  fields.forEach((field) => {
    let fieldSchema: z.ZodTypeAny

    switch (field.type) {
      case "email":
        fieldSchema = z.string().email(field.validation?.required || "Invalid email address")
        break
      case "number":
        fieldSchema = z.coerce.number()
        if (field.validation?.min) {
          fieldSchema = (fieldSchema as z.ZodNumber).min(
            field.validation.min.value,
            field.validation.min.message
          )
        }
        if (field.validation?.max) {
          fieldSchema = (fieldSchema as z.ZodNumber).max(
            field.validation.max.value,
            field.validation.max.message
          )
        }
        break
      case "date":
        fieldSchema = z.date({
          message: field.validation?.required || "Date is required",
        })
        break
      default:
        fieldSchema = z.string()
        if (field.validation?.required) {
          fieldSchema = (fieldSchema as z.ZodString).min(1, field.validation.required)
        }
        if (field.validation?.minLength) {
          fieldSchema = (fieldSchema as z.ZodString).min(
            field.validation.minLength.value,
            field.validation.minLength.message
          )
        }
        if (field.validation?.maxLength) {
          fieldSchema = (fieldSchema as z.ZodString).max(
            field.validation.maxLength.value,
            field.validation.maxLength.message
          )
        }
        if (field.validation?.pattern) {
          fieldSchema = (fieldSchema as z.ZodString).regex(
            field.validation.pattern.value,
            field.validation.pattern.message
          )
        }
    }

    schemaFields[field.name] = fieldSchema
  })

  return z.object(schemaFields)
}

export function MultiStepFormSimple({
  steps,
  formData,
  title = "Complete Your Registration",
  description = "Follow the steps below to get started",
  onComplete,
  successTitle = "Success!",
  successDescription = "Your information has been successfully submitted.",
}: MultiStepFormProps) {
  const [currentStep, setCurrentStep] = useState(1)
  const [showSuccess, setShowSuccess] = useState(false)
  const [collectedData, setCollectedData] = useState<Record<string, Record<string, FormFieldValue>>>({})

  // Get current step form data
  const currentStepData = formData.find((data) => data.stepId === currentStep)
  const currentStepFields = currentStepData?.fields || []

  // Create dynamic schema
  const schema = createZodSchema(currentStepFields)
  type FormData = z.infer<typeof schema>

  // Initialize form
  const form = useForm<FormData>({
    resolver: zodResolver(schema),
    defaultValues: collectedData[\`step\${currentStep}\`] || {},
  })

  const isLastStep = currentStep === steps.length

  const onSubmit = (data: FormData) => {
    const newData = { ...collectedData, [\`step\${currentStep}\`]: data as Record<string, FormFieldValue> }
    setCollectedData(newData)

    if (isLastStep) {
      // Final submission
      setShowSuccess(true)
      if (onComplete) {
        onComplete(newData)
      }
    } else {
      // Move to next step
      setCurrentStep(currentStep + 1)
      form.reset()
    }
  }

  const handleBack = () => {
    setCurrentStep(currentStep - 1)
    form.reset()
  }

  const handleReset = () => {
    setShowSuccess(false)
    setCurrentStep(1)
    setCollectedData({})
    form.reset()
  }

  // Render field based on type
  const renderField = (field: FieldConfig) => {
    return (
      <FormField
        key={field.name}
        control={form.control}
        name={field.name as keyof FormData}
        render={({ field: formField }) => (
          <FormItem className={field.gridColumn === "half" ? "col-span-1" : "col-span-2"}>
            <FormLabel className="text-sm font-semibold text-white/90">{field.label}</FormLabel>
            <FormControl>
              {field.type === "textarea" ? (
                <Textarea
                  placeholder={field.placeholder}
                  {...formField}
                  value={formField.value as string}
                  className="min-h-[100px]"
                />
              ) : field.type === "date" ? (
                <Popover>
                  <PopoverTrigger asChild>
                    <Button
                      variant="outline"
                      className={cn(
                        "h-11 pl-3 text-left font-normal w-full bg-white/10 backdrop-blur-md border-white/20 text-white hover:bg-white/15 hover:border-white/40 transition-all duration-300",
                        !formField.value && "text-white/60"
                      )}
                    >
                      {formField.value ? format(formField.value as Date, "PPP") : <span>Pick a date</span>}
                      <CalendarIcon className="ml-auto h-4 w-4 opacity-60" />
                    </Button>
                  </PopoverTrigger>
                  <PopoverContent 
                    className="w-auto p-0 bg-black/20 backdrop-blur-xl" 
                    align="start"
                  >
                    <Calendar
                      mode="single"
                      selected={formField.value as Date}
                      onSelect={formField.onChange}
                      disabled={(date) => date > new Date() || date < new Date("1900-01-01")}
                      initialFocus
                      className="pointer-events-auto bg-transparent"
                    />
                  </PopoverContent>
                </Popover>
              ) : field.type === "select" ? (
                <Select onValueChange={formField.onChange} defaultValue={formField.value as string}>
                  <SelectTrigger className="h-11 bg-white/10 backdrop-blur-md border-white/20 text-white hover:bg-white/15 hover:border-white/40 transition-all duration-300">
                    <SelectValue placeholder={field.placeholder || "Select an option"} className="text-white" />
                  </SelectTrigger>
                  <SelectContent 
                    className="bg-black/20 backdrop-blur-xl border-white/20"
                    style={{
                      background: "rgba(0, 0, 0, 0.2)",
                      backdropFilter: "blur(20px)",
                      border: "1px solid rgba(255, 255, 255, 0.2)",
                    }}
                  >
                    {field.options?.map((option) => (
                      <SelectItem 
                        key={option.value} 
                        value={option.value}
                        className="text-white focus:bg-white/20 focus:text-white"
                      >
                        {option.label}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              ) : (
                <Input
                  type={field.type}
                  placeholder={field.placeholder}
                  {...formField}
                  value={formField.value as string | number}
                  className="h-11 bg-white/10 backdrop-blur-md border-white/20 text-white placeholder:text-white/60 focus:bg-white/15 focus:border-white/40 transition-all duration-300"
                />
              )}
            </FormControl>
            <FormMessage className="text-red-300" />
          </FormItem>
        )}
      />
    )
  }

  return (
    <div className="min-h-screen flex items-center justify-center p-4 relative overflow-hidden">
      {/* Animated Liquid Glass Background */}
      <div className="absolute inset-0 -z-10">
        {/* Base gradient */}
        {/* <div className="absolute inset-0 bg-gradient-to-br from-blue-500/20 via-purple-500/20 to-pink-500/20" /> */}
        
        {/* Animated glass bubbles */}
        {/* <div className="absolute top-1/4 left-1/4 w-64 h-64 bg-gray-400/10 rounded-full blur-3xl animate-pulse" />
        <div className="absolute top-1/3 right-1/4 w-80 h-80 bg-gray-400/10 rounded-full blur-3xl animate-pulse delay-1000" />
        <div className="absolute bottom-1/4 left-1/3 w-72 h-72 bg-gray-400/10 rounded-full blur-3xl animate-pulse delay-500" />
        <div className="absolute bottom-1/3 right-1/3 w-96 h-96 bg-gray-400/10 rounded-full blur-3xl animate-pulse delay-1500" /> */}
        
        {/* Noise texture for authentic glass effect */}
      </div>

      {/* Main Card with Liquid Glass Effect */}
      <Card 
        className="w-full max-w-3xl border-white/20 bg-black/10 backdrop-blur-xl shadow-2xl relative overflow-hidden"
        style={{
          // background: "rgba(0, 0, 0, 0.1)",
          // backdropFilter: "blur(20px)",
          // border: "1px solid rgba(255, 255, 255, 0.2)",
          // boxShadow: "0 8px 32px 0 rgba(31, 38, 135, 0.37)",
        }}
      >
        {/* Inner shine effect */}
        {/* <div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent pointer-events-none" /> */}
        
        {/* Subtle border glow */}
        <div className="absolute inset-0 rounded-xl bg-gray-400/10 blur-sm opacity-50 pointer-events-none" />

        <CardHeader className="pb-8 relative z-10">
          <CardTitle className="text-3xl font-bold bg-gradient-to-r from-white to-white/80 bg-clip-text text-transparent">
            {title}
          </CardTitle>
          <CardDescription className="text-base mt-2 text-white/80">
            {description}
          </CardDescription>
        </CardHeader>

        {/* Step Indicator */}
        <div className="px-6 pb-12 relative z-10">
          <StepIndicator steps={steps} currentStep={currentStep} />
        </div>

        <CardContent className="pb-8 relative z-10">
          <div className="animate-in fade-in duration-300">
            <Form {...form}>
              <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
                <div className="grid grid-cols-2 gap-4">
                  {currentStepFields.map((field) => renderField(field))}
                </div>

                <div className={cn("flex pt-6 gap-4", currentStep === 1 ? "justify-end" : "justify-between")}>
                  {currentStep > 1 && (
                    <Button 
                      type="button" 
                      variant="outline" 
                      size="lg" 
                      onClick={handleBack} 
                      className="gap-2 bg-white/10 backdrop-blur-md border-white/20 text-white hover:bg-white/20 hover:border-white/40 transition-all duration-300"
                    >
                      <ArrowLeft className="w-4 h-4" />
                      Back
                    </Button>
                  )}
                  <BtnShimmer
                    onClick={form.handleSubmit(onSubmit)}
                  >
                    Submit
                  </BtnShimmer>
                </div>
              </form>
            </Form>
          </div>
        </CardContent>
      </Card>

      {/* Success Dialog with Liquid Glass Effect */}
      <Dialog open={showSuccess} onOpenChange={setShowSuccess}>
        <DialogContent 
          className="sm:max-w-md border-white/20 bg-black/20 backdrop-blur-xl"
          style={{
            background: "rgba(0, 0, 0, 0.2)",
            backdropFilter: "blur(20px)",
            border: "1px solid rgba(255, 255, 255, 0.2)",
            boxShadow: "0 8px 32px 0 rgba(31, 38, 135, 0.37)",
          }}
        >
          {/* Inner shine for dialog */}
          <div className="absolute inset-0 bg-gradient-to-br from-white/5 to-transparent pointer-events-none rounded-lg" />
          
          <DialogHeader className="text-center relative z-10">
            <div className="mx-auto mb-4 w-16 h-16 bg-gradient-to-r from-green-400 to-emerald-500 rounded-full flex items-center justify-center shadow-lg">
              <Check className="w-10 h-10 text-white" />
            </div>
            <DialogTitle className="text-2xl bg-gradient-to-r from-white to-white/80 bg-clip-text text-transparent">
              {successTitle}
            </DialogTitle>
            <DialogDescription className="text-base pt-2 text-white/80">
              {successDescription}
            </DialogDescription>
          </DialogHeader>
          <div className="flex justify-center pt-4 relative z-10">
            <Button 
              className="bg-gradient-to-r from-blue-500/90 to-purple-500/90 backdrop-blur-md border-white/20 text-white hover:from-blue-600/90 hover:to-purple-600/90 hover:scale-105 transform transition-all duration-300"
              onClick={handleReset}
            >
              Start New
            </Button>
          </div>
        </DialogContent>
      </Dialog>
    </div>
  )
}

Step Indicator Component

Copy this code to components/aesthe-ui/multi-form/step-indicator.tsx:

'use client'
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"

interface Step {
  id: number
  name: string
  description: string
}

interface StepIndicatorProps {
  steps: Step[]
  currentStep: number
}

export function StepIndicator({ steps, currentStep }: StepIndicatorProps) {
  const progress = ((currentStep - 1) / (steps.length - 1)) * 100

  return (
    <div className="w-full relative">
      {/* Background thread line */}
      <div className="absolute top-5 left-0 right-0 h-1 flex items-center px-6">
        <div className="w-full h-full bg-border rounded-full" />
      </div>

      {/* Active thread line */}
      <div className="absolute top-5 left-0 right-0 h-1 flex items-center px-6">
        <div
          className="h-full bg-primary rounded-full transition-all duration-500 ease-out"
          style={{
            width: \`\${progress}%\`,
          }}
        />
      </div>

      <div className="flex justify-between relative">
        {steps.map((step) => {
          const isCompleted = currentStep > step.id
          const isActive = currentStep === step.id

          return (
            <div key={step.id} className="flex flex-col items-center">
              {/* Step circle */}
              <div
                className={cn(
                  "w-12 h-12 rounded-full flex items-center justify-center font-semibold text-sm transition-all duration-300 shadow-md",
                  isCompleted
                    ? "bg-primary text-primary-foreground ring-2 ring-primary ring-offset-2"
                    : isActive
                      ? "bg-primary text-primary-foreground ring-2 ring-primary ring-offset-2 scale-110"
                      : "bg-card text-muted-foreground border-2 border-border"
                )}
              >
                {isCompleted ? <Check className="w-5 h-5" /> : <span>{step.id}</span>}
              </div>

              {/* Step labels */}
              <div className="text-center mt-3 w-[100px]">
                <p
                  className={cn(
                    "text-sm font-medium transition-colors duration-300",
                    isActive || isCompleted ? "text-foreground" : "text-muted-foreground",
                  )}
                >
                  {step.name}
                </p>
                <p className="text-xs text-muted-foreground">{step.description}</p>
              </div>
            </div>
          )
        })}
      </div>
    </div>
  )
}

Field Types Reference

TypeDescriptionExample
textStandard text inputName, address, etc.
emailEmail input with validationEmail addresses
numberNumeric inputAge, quantity, etc.
telTelephone number inputPhone numbers
dateDate picker with calendarBirth date, appointments
textareaMulti-line text areaBio, comments, descriptions
selectDropdown select menuCountry, category, etc.

Validation Rules

All fields support comprehensive validation:

  • required: Field must be filled
  • minLength: Minimum character length
  • maxLength: Maximum character length
  • min: Minimum value (for numbers)
  • max: Maximum value (for numbers)
  • pattern: Custom regex pattern matching

Grid Layout

Control field width with gridColumn:

  • "full" - Spans 2 columns (full width)
  • "half" - Spans 1 column (half width)
{
  name: "address",
  gridColumn: "full", // Takes full width
  // ...
}

{
  name: "city",
  gridColumn: "half", // Takes half width
  // ...
}

Styling

The component supports both glass morphism and clean variants:

  • Glass Morphism (multi-step-form.tsx) - Frosted glass effects with liquid backgrounds
  • Clean (multi-step-form-simple.tsx) - Minimal design with focus on content

Both variants are fully themeable and support dark mode.


API Response Format

When form is completed, data is structured by step:

{
  "step1": {
    "fullName": "John Doe",
    "email": "john@example.com",
    "dateOfBirth": "1990-01-01T00:00:00.000Z"
  },
  "step2": {
    "address": "123 Main St",
    "city": "New York",
    "zipCode": "10001"
  },
  "step3": {
    "country": "us",
    "bio": "Software developer..."
  }
}

Tips & Best Practices

  1. Keep steps logical - Group related fields together
  2. Limit fields per step - 3-5 fields per step is ideal
  3. Use appropriate field types - Improves UX and validation
  4. Provide helpful placeholders - Guide users on expected format
  5. Write clear validation messages - Help users correct errors
  6. Test on mobile - Ensure forms work well on small screens
  7. Consider progressive disclosure - Show only relevant fields

Accessibility

The component includes:

  • Proper ARIA labels
  • Keyboard navigation support
  • Focus management
  • Screen reader friendly
  • Error announcements
  • High contrast support

Examples

Check out the live examples:

  • Minimal Example: demo-minimal.tsx - Basic 3-step form
  • Full Example: demo-simple.tsx - Complete registration flow

Copy these examples to get started quickly!