Forms
Form components and validation
Forms
Build forms with validation using React Hook Form and Zod.
Installation
pnpm add react-hook-form zod @hookform/resolversBasic Form
'use client';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
const formSchema = z.object({
email: z.string().email('Invalid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type FormData = z.infer<typeof formSchema>;
export function LoginForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = async (data: FormData) => {
console.log(data);
// Handle form submission
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium mb-2">
Email
</label>
<Input
id="email"
type="email"
{...register('email')}
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && (
<p className="text-red-500 text-sm mt-1">{errors.email.message}</p>
)}
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium mb-2">
Password
</label>
<Input
id="password"
type="password"
{...register('password')}
className={errors.password ? 'border-red-500' : ''}
/>
{errors.password && (
<p className="text-red-500 text-sm mt-1">{errors.password.message}</p>
)}
</div>
<Button type="submit" disabled={isSubmitting} className="w-full">
{isSubmitting ? 'Submitting...' : 'Sign In'}
</Button>
</form>
);
}Form with shadcn/ui
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
export function ProfileForm() {
const form = useForm<FormData>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: '',
},
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="John Doe" {...field} />
</FormControl>
<FormDescription>
Your public display name
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Update Profile</Button>
</form>
</Form>
);
}Input Components
Text Input
<Input
type="text"
placeholder="Enter text"
{...register('fieldName')}
/>Textarea
<Textarea
placeholder="Enter description"
rows={5}
{...register('description')}
/>Select
<select {...register('country')} className="border rounded px-3 py-2">
<option value="">Select country</option>
<option value="us">United States</option>
<option value="uk">United Kingdom</option>
</select>Checkbox
<div className="flex items-center space-x-2">
<input
type="checkbox"
id="terms"
{...register('terms')}
className="rounded border-gray-300"
/>
<label htmlFor="terms" className="text-sm">
I agree to the terms and conditions
</label>
</div>Radio
<div className="space-y-2">
<div className="flex items-center space-x-2">
<input
type="radio"
id="option1"
value="option1"
{...register('choice')}
/>
<label htmlFor="option1">Option 1</label>
</div>
<div className="flex items-center space-x-2">
<input
type="radio"
id="option2"
value="option2"
{...register('choice')}
/>
<label htmlFor="option2">Option 2</label>
</div>
</div>Complex Validation
const schema = z.object({
email: z.string().email(),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
confirmPassword: z.string(),
age: z.number().min(18, 'Must be at least 18'),
website: z.string().url().optional(),
terms: z.boolean().refine((val) => val === true, {
message: 'You must accept the terms',
}),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword'],
});File Upload
const fileSchema = z.object({
file: z.instanceof(FileList).refine((files) => files.length > 0, {
message: 'File is required',
}),
});
export function FileUploadForm() {
const { register, handleSubmit } = useForm({
resolver: zodResolver(fileSchema),
});
const onSubmit = async (data: any) => {
const file = data.file[0];
const formData = new FormData();
formData.append('file', file);
await fetch('/api/upload', {
method: 'POST',
body: formData,
});
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input type="file" {...register('file')} />
<Button type="submit">Upload</Button>
</form>
);
}Dynamic Fields
import { useFieldArray } from 'react-hook-form';
export function DynamicForm() {
const { control, register } = useForm({
defaultValues: {
items: [{ name: '', quantity: 0 }],
},
});
const { fields, append, remove } = useFieldArray({
control,
name: 'items',
});
return (
<form>
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<Input {...register(`items.${index}.name`)} placeholder="Item name" />
<Input
type="number"
{...register(`items.${index}.quantity`, { valueAsNumber: true })}
placeholder="Quantity"
/>
<Button type="button" onClick={() => remove(index)}>
Remove
</Button>
</div>
))}
<Button
type="button"
onClick={() => append({ name: '', quantity: 0 })}
>
Add Item
</Button>
</form>
);
}Server Actions
'use client';
import { useFormState } from 'react-dom';
import { createUser } from '@/app/actions/user';
export function ServerActionForm() {
const [state, formAction] = useFormState(createUser, null);
return (
<form action={formAction} className="space-y-4">
<div>
<label htmlFor="name">Name</label>
<Input id="name" name="name" required />
</div>
<div>
<label htmlFor="email">Email</label>
<Input id="email" name="email" type="email" required />
</div>
{state?.error && (
<p className="text-red-500">{state.error}</p>
)}
<Button type="submit">Create User</Button>
</form>
);
}Best Practices
- Validate on both sides: Client and server
- Clear error messages: Help users fix errors
- Loading states: Show feedback during submission
- Disable on submit: Prevent duplicate submissions
- Reset on success: Clear form after successful submission
- Accessible labels: Use proper form labels
- Focus management: Focus on first error field
Next Steps
- Learn about Button
- Explore Dialog
- Read about Validation