Form
Build accessible, type-safe forms with React Hook Form and Zod validation. Composable components that handle state, validation, and accessibility automatically.
Building forms in React used to suck. This setup combines React Hook Form with Zod validation so you get type safety, error handling, and all that good stuff without the usual headaches.
Simple form with validation
A clean, accessible form with real-time validation:
React Hook Form handles the state, Zod catches the errors, and the accessibility stuff happens automatically.
npx shadcn@latest add form
Why this doesn't suck like other form libraries
You've probably tried building forms in React before and wanted to give up. This actually works:
- Type safety - Zod catches your mistakes before users see them
- Fast - React Hook Form doesn't re-render everything on every keystroke
- Actually accessible - Screen readers work, keyboard nav works
- Composable - Mix and match components however you need
- Real-time validation - Show errors as people type, not after they submit
- Server-side too - Validate on both ends without duplicate code
- Just React - Not tied to Next.js or any other framework
Essential form patterns you'll build
Profile form
Username, email, bio - the full profile setup:
Select dropdown
Dropdowns that actually validate:
Checkbox groups
Pick multiple options, validate the whole array:
Combobox search
Search and select with validation:
Date picker
Date picker that actually validates dates:
How to actually use this
1. Write your schema
Tell Zod what you expect:
import { z } from "zod"
const formSchema = z.object({
username: z.string().min(2, {
message: "Username must be at least 2 characters.",
}),
email: z.string().email({
message: "Please enter a valid email address.",
}),
})
2. Hook it up
Connect React Hook Form:
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
username: "",
email: "",
},
})
3. Build the thing
Put it all together:
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
API Reference
Form
Root form provider that connects React Hook Form with form components.
Prop | Type | Description |
---|---|---|
...formProps | UseFormReturn<TFieldValues> | React Hook Form return object |
children | React.ReactNode | Form content and fields |
FormField
Controlled form field that handles validation and state.
Prop | Type | Description |
---|---|---|
control | Control<TFieldValues> | Form control from useForm |
name | Path<TFieldValues> | Field name matching schema |
render | ({ field, fieldState, formState }) => React.ReactNode | Render function for field UI |
shouldUnregister | boolean | Whether to unregister field on unmount |
defaultValue | TFieldValue | Default value for uncontrolled usage |
rules | RegisterOptions | Additional validation rules |
FormItem
Container for form field with proper spacing and context.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | FormLabel, FormControl, FormMessage |
FormLabel
Accessible label that connects to form field automatically.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Label text |
FormControl
Wrapper that provides field connection and accessibility attributes.
Prop | Type | Default | Description |
---|---|---|---|
children | React.ReactElement | - | Single form input component |
FormDescription
Optional helper text that provides additional context.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Description text |
FormMessage
Error message that displays validation feedback automatically.
Prop | Type | Default | Description |
---|---|---|---|
className | string | - | Additional CSS classes |
children | React.ReactNode | - | Custom error message (optional) |
Validation Modes
Mode | Description | Use Case |
---|---|---|
onSubmit | Validate on form submission | Default, best performance |
onBlur | Validate when field loses focus | Better UX for long forms |
onChange | Validate on every change | Real-time feedback needed |
onTouched | Validate after first interaction | Balance of UX and performance |
all | Validate on all events | Maximum feedback |
Field State Properties
Property | Type | Description |
---|---|---|
field.value | any | Current field value |
field.onChange | (value: any) => void | Change handler |
field.onBlur | () => void | Blur handler |
field.name | string | Field name |
field.ref | React.Ref | Field reference |
fieldState.invalid | boolean | Whether field has errors |
fieldState.isTouched | boolean | Whether field was interacted with |
fieldState.isDirty | boolean | Whether field value changed |
fieldState.error | FieldError | Validation error object |
Common Validation Patterns
String validation
z.string()
.min(2, "Too short")
.max(50, "Too long")
.email("Invalid email")
.url("Invalid URL")
.regex(/^[A-Z0-9]+$/, "Uppercase letters and numbers only")
Number validation
z.number()
.min(0, "Must be positive")
.max(100, "Must be under 100")
.int("Must be whole number")
Array validation
z.array(z.string())
.min(1, "Select at least one")
.max(5, "Select up to 5 items")
Object validation
z.object({
name: z.string().min(1),
email: z.string().email(),
}).refine((data) => data.name !== data.email, {
message: "Name and email cannot be the same",
path: ["name"],
})
Form best practices
Create forms users actually want to fill out:
- Clear labels - Use descriptive, action-oriented labels that explain what's needed
- Helpful placeholders - Show format examples, not just repeat the label
- Smart defaults - Pre-fill fields when you have the information
- Real-time validation - Catch errors early, but don't be overly aggressive
- Progress indicators - Show users how much more they need to complete
- Error recovery - Make it easy to understand and fix validation errors
- Logical grouping - Organize related fields together with clear sections
- Mobile optimization - Use appropriate input types and keyboard layouts
- Save progress - Allow users to continue later for longer forms
Dropdown Menu
A versatile dropdown menu component for actions, navigation, and settings. Built on Radix UI with customizable positioning and rich interactions.
Hover Card
A contextual popup that appears on hover, perfect for previewing content, user profiles, and additional information without leaving the current page.