New

Multi-Step Form

A 3-step form wizard with progress indicators, validation, and step navigation.

Forms & Inputformwizardstepsprogressvalidation

Dependencies

Other dependencies:

@/components/ui/card@/components/ui/button@/components/ui/input@/components/ui/label@/components/ui/progress

How to use this component

Copy the code below into your project. Make sure you have the required shadcn/ui dependencies installed. Then import and use the component in your pages or layouts.

Code

1"use client";
2
3import { useState } from 'react';
4import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5import { Button } from '@/components/ui/button';
6import { Input } from '@/components/ui/input';
7import { Label } from '@/components/ui/label';
8import { Progress } from '@/components/ui/progress';
9import { Check, ChevronRight, ChevronLeft } from 'lucide-react';
10
11export default function MultiStepForm() {
12 const [currentStep, setCurrentStep] = useState(1);
13 const [formData, setFormData] = useState({
14 name: '',
15 email: '',
16 street: '',
17 city: '',
18 zip: ''
19 });
20 const [errors, setErrors] = useState<Record<string, string>>({});
21
22 const updateField = (field: string, value: string) => {
23 setFormData(prev => ({ ...prev, [field]: value }));
24 if (errors[field]) {
25 setErrors(prev => ({ ...prev, [field]: '' }));
26 }
27 };
28
29 const validateStep = (step: number) => {
30 const newErrors: Record<string, string> = {};
31
32 if (step === 1) {
33 if (!formData.name.trim()) newErrors.name = 'Name is required';
34 if (!formData.email.trim()) newErrors.email = 'Email is required';
35 } else if (step === 2) {
36 if (!formData.street.trim()) newErrors.street = 'Street is required';
37 if (!formData.city.trim()) newErrors.city = 'City is required';
38 if (!formData.zip.trim()) newErrors.zip = 'ZIP is required';
39 }
40
41 setErrors(newErrors);
42 return Object.keys(newErrors).length === 0;
43 };
44
45 const nextStep = () => {
46 if (validateStep(currentStep) && currentStep < 3) {
47 setCurrentStep(prev => prev + 1);
48 }
49 };
50
51 const prevStep = () => {
52 if (currentStep > 1) {
53 setCurrentStep(prev => prev - 1);
54 }
55 };
56
57 const steps = [
58 { id: 1, title: 'Personal' },
59 { id: 2, title: 'Address' },
60 { id: 3, title: 'Review' }
61 ];
62
63 return (
64 <Card className="w-full max-w-lg mx-auto">
65 <CardHeader>
66 <CardTitle>Create Account</CardTitle>
67 </CardHeader>
68 <CardContent className="space-y-6">
69 <div className="space-y-2">
70 <div className="flex items-center justify-between">
71 {steps.map((step, idx) => (
72 <div key={step.id} className="flex items-center flex-1">
73 <div className={`flex items-center justify-center w-8 h-8 rounded-full border-2 \` +
74 `${currentStep > step.id ? 'border-green-500 bg-green-500 text-white' : ''}\` +
75 `${currentStep === step.id ? 'border-primary bg-primary text-primary-foreground' : ''}\` +
76 `${currentStep < step.id ? 'border-muted text-muted-foreground' : ''}`}>
77 {currentStep > step.id ? <Check className="w-4 h-4" /> : step.id}
78 </div>
79 {idx < steps.length - 1 && (
80 <div className={`flex-1 h-0.5 mx-2 \` +
81 `${currentStep > step.id ? 'bg-green-500' : 'bg-muted'}`} />
82 )}
83 </div>
84 ))}
85 </div>
86 <Progress value={(currentStep / 3) * 100} className="h-2" />
87 </div>
88
89 {currentStep === 1 && (
90 <div className="space-y-4">
91 <div className="space-y-2">
92 <Label htmlFor="name">Full Name</Label>
93 <Input
94 id="name"
95 value={formData.name}
96 onChange={e => updateField('name', e.target.value)}
97 className={errors.name ? 'border-red-500' : ''}
98 placeholder="John Doe"
99 />
100 {errors.name && <p className="text-sm text-red-500">{errors.name}</p>}
101 </div>
102 <div className="space-y-2">
103 <Label htmlFor="email">Email</Label>
104 <Input
105 id="email"
106 type="email"
107 value={formData.email}
108 onChange={e => updateField('email', e.target.value)}
109 className={errors.email ? 'border-red-500' : ''}
110 placeholder="john@example.com"
111 />
112 {errors.email && <p className="text-sm text-red-500">{errors.email}</p>}
113 </div>
114 </div>
115 )}
116
117 {currentStep === 2 && (
118 <div className="space-y-4">
119 <div className="space-y-2">
120 <Label htmlFor="street">Street Address</Label>
121 <Input
122 id="street"
123 value={formData.street}
124 onChange={e => updateField('street', e.target.value)}
125 className={errors.street ? 'border-red-500' : ''}
126 placeholder="123 Main St"
127 />
128 {errors.street && <p className="text-sm text-red-500">{errors.street}</p>}
129 </div>
130 <div className="grid grid-cols-2 gap-4">
131 <div className="space-y-2">
132 <Label htmlFor="city">City</Label>
133 <Input
134 id="city"
135 value={formData.city}
136 onChange={e => updateField('city', e.target.value)}
137 className={errors.city ? 'border-red-500' : ''}
138 placeholder="New York"
139 />
140 {errors.city && <p className="text-sm text-red-500">{errors.city}</p>}
141 </div>
142 <div className="space-y-2">
143 <Label htmlFor="zip">ZIP Code</Label>
144 <Input
145 id="zip"
146 value={formData.zip}
147 onChange={e => updateField('zip', e.target.value)}
148 className={errors.zip ? 'border-red-500' : ''}
149 placeholder="10001"
150 />
151 {errors.zip && <p className="text-sm text-red-500">{errors.zip}</p>}
152 </div>
153 </div>
154 </div>
155 )}
156
157 {currentStep === 3 && (
158 <div className="space-y-4">
159 <h3 className="font-semibold">Review Your Information</h3>
160 <div className="bg-muted p-4 rounded-lg space-y-2">
161 <div><span className="text-muted-foreground">Name:</span> {formData.name}</div>
162 <div><span className="text-muted-foreground">Email:</span> {formData.email}</div>
163 <div><span className="text-muted-foreground">Address:</span> {formData.street}</div>
164 <div><span className="text-muted-foreground">City:</span> {formData.city}</div>
165 <div><span className="text-muted-foreground">ZIP:</span> {formData.zip}</div>
166 </div>
167 </div>
168 )}
169
170 <div className="flex justify-between pt-4">
171 <Button
172 variant="outline"
173 onClick={prevStep}
174 disabled={currentStep === 1}
175 >
176 <ChevronLeft className="w-4 h-4 mr-2" />
177 Previous
178 </Button>
179 {currentStep < 3 ? (
180 <Button onClick={nextStep}>
181 Next
182 <ChevronRight className="w-4 h-4 ml-2" />
183 </Button>
184 ) : (
185 <Button onClick={() => alert('Form submitted!')}>
186 Submit
187 </Button>
188 )}
189 </div>
190 </CardContent>
191 </Card>
192 );
193}

Related Forms & Input Components

Command Palette

Search for a command to run...