Overview
The form system combines react-hook-form for form state management, Zod for schema validation, and shadcn/ui components for accessible, styled inputs. This creates a powerful, type-safe form solution.
Form components are located in src/components/ui/form.tsx, input.tsx, label.tsx, and textarea.tsx
Core Dependencies
{
"react-hook-form" : "^7.61.1" ,
"@hookform/resolvers" : "^3.10.0" ,
"zod" : "^3.25.76"
}
The Form component wraps react-hook-form’s FormProvider:
import { FormProvider } from "react-hook-form"
const Form = FormProvider
Location: src/components/ui/input.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React . forwardRef < HTMLInputElement , React . ComponentProps < "input" >>(
({ className , type , ... props }, ref ) => {
return (
< input
type = { type }
className = { cn (
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-base ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 md:text-sm" ,
className
) }
ref = { ref }
{ ... props }
/>
)
}
)
Height : 40px (h-10) for comfortable touch targets
Border : Themed border color with 2px focus ring
States : Hover, focus, disabled, and error states
File inputs : Special styling for file upload inputs
Responsive : Larger text on mobile, smaller on desktop
Label Component
Location: src/components/ui/label.tsx
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva , type VariantProps } from "class-variance-authority"
const labelVariants = cva (
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React . forwardRef <
React . ElementRef < typeof LabelPrimitive . Root > ,
React . ComponentPropsWithoutRef < typeof LabelPrimitive . Root > & VariantProps < typeof labelVariants >
> (({ className , ... props }, ref ) => (
< LabelPrimitive.Root ref = { ref } className = { cn ( labelVariants (), className ) } { ... props } />
))
Textarea Component
Location: src/components/ui/textarea.tsx
import * as React from "react"
import { cn } from "@/lib/utils"
const Textarea = React . forwardRef < HTMLTextAreaElement , React . ComponentProps < "textarea" >>(
({ className , ... props }, ref ) => {
return (
< textarea
className = { cn (
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" ,
className
) }
ref = { ref }
{ ... props }
/>
)
}
)
Step 1: Define Schema with Zod
import { z } from "zod"
const contactFormSchema = z . object ({
name: z . string (). min ( 2 , {
message: "Name must be at least 2 characters." ,
}),
email: z . string (). email ({
message: "Please enter a valid email address." ,
}),
message: z . string (). min ( 10 , {
message: "Message must be at least 10 characters." ,
}),
})
type ContactFormValues = z . infer < typeof contactFormSchema >
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
function ContactForm () {
const form = useForm < ContactFormValues >({
resolver: zodResolver ( contactFormSchema ),
defaultValues: {
name: "" ,
email: "" ,
message: "" ,
},
})
function onSubmit ( values : ContactFormValues ) {
console . log ( values )
// Handle form submission
}
return (
< Form { ... form } >
< form onSubmit = { form . handleSubmit ( onSubmit ) } className = "space-y-6" >
{ /* Form fields */ }
</ form >
</ Form >
)
}
import {
Form ,
FormControl ,
FormDescription ,
FormField ,
FormItem ,
FormLabel ,
FormMessage ,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Button } from "@/components/ui/button"
< Form { ... form } >
< form onSubmit = { form . handleSubmit ( onSubmit ) } className = "space-y-6" >
{ /* Name Field */ }
< FormField
control = { form . control }
name = "name"
render = { ({ field }) => (
< FormItem >
< FormLabel > Name </ FormLabel >
< FormControl >
< Input placeholder = "John Doe" { ... field } />
</ FormControl >
< FormDescription >
Your full name as you'd like it to appear.
</ FormDescription >
< FormMessage />
</ FormItem >
) }
/>
{ /* Email Field */ }
< FormField
control = { form . control }
name = "email"
render = { ({ field }) => (
< FormItem >
< FormLabel > Email </ FormLabel >
< FormControl >
< Input type = "email" placeholder = "[email protected] " { ... field } />
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
{ /* Message Field */ }
< FormField
control = { form . control }
name = "message"
render = { ({ field }) => (
< FormItem >
< FormLabel > Message </ FormLabel >
< FormControl >
< Textarea
placeholder = "Your message here..."
className = "min-h-[120px]"
{ ... field }
/>
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
< Button type = "submit" disabled = { form . formState . isSubmitting } >
{ form . formState . isSubmitting ? "Sending..." : "Send Message" }
</ Button >
</ form >
</ Form >
The project includes many other form components:
Select Component
import {
Select ,
SelectContent ,
SelectItem ,
SelectTrigger ,
SelectValue ,
} from "@/components/ui/select"
< FormField
control = { form . control }
name = "country"
render = { ({ field }) => (
< FormItem >
< FormLabel > Country </ FormLabel >
< Select onValueChange = { field . onChange } defaultValue = { field . value } >
< FormControl >
< SelectTrigger >
< SelectValue placeholder = "Select a country" />
</ SelectTrigger >
</ FormControl >
< SelectContent >
< SelectItem value = "us" > United States </ SelectItem >
< SelectItem value = "uk" > United Kingdom </ SelectItem >
< SelectItem value = "es" > Spain </ SelectItem >
</ SelectContent >
</ Select >
< FormMessage />
</ FormItem >
) }
/>
Checkbox Component
import { Checkbox } from "@/components/ui/checkbox"
< FormField
control = { form . control }
name = "terms"
render = { ({ field }) => (
< FormItem className = "flex flex-row items-start space-x-3 space-y-0" >
< FormControl >
< Checkbox
checked = { field . value }
onCheckedChange = { field . onChange }
/>
</ FormControl >
< div className = "space-y-1 leading-none" >
< FormLabel >
Accept terms and conditions
</ FormLabel >
< FormDescription >
You agree to our Terms of Service and Privacy Policy.
</ FormDescription >
</ div >
</ FormItem >
) }
/>
Radio Group Component
import { RadioGroup , RadioGroupItem } from "@/components/ui/radio-group"
< FormField
control = { form . control }
name = "plan"
render = { ({ field }) => (
< FormItem className = "space-y-3" >
< FormLabel > Select a plan </ FormLabel >
< FormControl >
< RadioGroup
onValueChange = { field . onChange }
defaultValue = { field . value }
className = "flex flex-col space-y-1"
>
< FormItem className = "flex items-center space-x-3 space-y-0" >
< FormControl >
< RadioGroupItem value = "free" />
</ FormControl >
< FormLabel className = "font-normal" >
Free
</ FormLabel >
</ FormItem >
< FormItem className = "flex items-center space-x-3 space-y-0" >
< FormControl >
< RadioGroupItem value = "pro" />
</ FormControl >
< FormLabel className = "font-normal" >
Pro
</ FormLabel >
</ FormItem >
</ RadioGroup >
</ FormControl >
< FormMessage />
</ FormItem >
) }
/>
Switch Component
import { Switch } from "@/components/ui/switch"
< FormField
control = { form . control }
name = "notifications"
render = { ({ field }) => (
< FormItem className = "flex flex-row items-center justify-between rounded-lg border p-4" >
< div className = "space-y-0.5" >
< FormLabel className = "text-base" >
Email Notifications
</ FormLabel >
< FormDescription >
Receive emails about your account activity.
</ FormDescription >
</ div >
< FormControl >
< Switch
checked = { field . value }
onCheckedChange = { field . onChange }
/>
</ FormControl >
</ FormItem >
) }
/>
Advanced Validation
Custom Validation
const schema = z . object ({
password: z . string (). min ( 8 ),
confirmPassword: z . string (),
}). refine (( data ) => data . password === data . confirmPassword , {
message: "Passwords don't match" ,
path: [ "confirmPassword" ],
})
Async Validation
const schema = z . object ({
username: z . string (). refine (
async ( username ) => {
const response = await fetch ( `/api/check-username?username= ${ username } ` )
return response . ok
},
{ message: "Username already taken" }
),
})
Accessibility Features
ARIA Attributes
aria-describedby for descriptions
aria-invalid for errors
Proper label associations
Keyboard Navigation
Tab navigation
Enter to submit
Escape to clear (where applicable)
Screen Readers
Semantic HTML
Error announcements
Field descriptions
Visual Feedback
Focus indicators
Error states
Disabled states
Best Practices
Always provide helpful error messages that guide users to fix validation issues.
Use descriptive labels and placeholders
Provide inline validation feedback
Group related fields together
Use appropriate input types (email, tel, url)
Handle loading states during submission
Prevent double submissions
Clear forms after successful submission
Button - Form submit buttons
Cards - Card containers for forms
Animations - Animation patterns for form interactions