React Hook Form
Form integration with React Hook Form library.
When to Use
Use React Hook Form when you need to:
- Build complex forms with schema-based validation using Zod
- Manage forms with many interdependent fields that need performant re-rendering
- Integrate validation across multiple input types (inputs, selects, checkboxes, dates, comboboxes)
- Provide real-time or on-submit validation with accessible error messages
- Handle form state (dirty, touched, errors) with minimal boilerplate
When Not to Use
- Simple single-field forms without validation - use the Field component directly
- Forms where you prefer a headless, framework-agnostic approach - consider TanStack Form
- Static display forms or read-only views - use Label with plain inputs
- Server-only form actions (Next.js Server Actions) - use the native Form component
Simple Form
Basic form with a single input field and Zod validation. The minimal setup for React Hook Form integration.
Contact Form
Multi-field form with Input and Textarea components, each validated through a shared Zod schema.
Multiple Input Types
Demonstrates integration with Input, Select, Checkbox, and Switch components in a single form.
Radio Group
Form with RadioGroup for single-choice selection, validated via a Zod enum.
Date Picker
Calendar-based date picker integrated with React Hook Form using a Popover trigger.
Combobox
Searchable combobox selector using Command and Popover components with form validation.
UX & Design Guidelines
Visual Hierarchy
Group related fields together using consistent spacing. Place the most important fields first and use FormDescription to provide context beneath each field. Use FormMessage for validation errors directly below their fields so users can quickly scan and fix issues.
Spacing & Layout
Use space-y-4 between form fields for comfortable vertical rhythm. For inline fields (e.g., Checkbox with label), use flex flex-row items-start space-x-3. Place the submit button at the bottom with pt-2 or more to visually separate it from form fields.
Responsive Behavior
Stack all form fields vertically on mobile for single-column readability. On larger screens, you may arrange short fields (e.g., first name / last name) side by side using grid grid-cols-2 gap-4. Ensure touch targets for checkboxes, switches, and radio buttons meet the minimum 44x44px guideline.
Color & Contrast
Error messages use text-destructive for clear visibility against both light and dark backgrounds. Description text uses text-muted-foreground to differentiate helper text from labels. Invalid fields display a red border ring to provide an additional visual cue beyond the error message alone.
Accessibility
Keyboard Navigation
- Tab — Move focus between form fields in DOM order
- Enter — Submit the form when a submit button is focused
- Space — Toggle checkboxes, switches, and radio items
- Arrow keys — Navigate between radio group options
- Escape — Close popover-based fields (date picker, combobox)
Screen Reader Support
FormLabelautomatically associates withFormControlvia generatedhtmlFor/idpairsFormDescriptionis linked viaaria-describedbyso assistive tech reads helper text alongside the field- Error messages use
role="alert"to immediately announce validation errors - Invalid fields receive
aria-invalid="true"automatically when validation fails
Focus Management
When shouldFocusError is enabled (default), the first field with a validation error is automatically focused after submission. This helps keyboard and screen reader users immediately locate and fix the issue. Visible focus rings use focus-visible:ring-[3px] for all form controls.
Error Announcements
Validation errors rendered by FormMessage are wrapped in an element with role="alert", ensuring they are announced by screen readers immediately when they appear. Each error is also linked to its field via aria-describedby for contextual association.
ARIA Attributes
{/* FormLabel auto-associates with FormControl via htmlFor */}
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
{/* Renders: <input id="form-item-email" aria-describedby="form-item-email-description" /> */}
<Input {...field} />
</FormControl>
{/* Renders: <p id="form-item-email-description"> */}
<FormDescription>We will never share your email.</FormDescription>
{/* When invalid, renders: <p id="form-item-email-error" role="alert"> */}
<FormMessage />
</FormItem>
)}
/>
{/* Invalid fields automatically receive aria-invalid="true" */}
{/* Error messages are announced to screen readers via role="alert" */}Related Components
API Reference
Key options for the useForm hook from React Hook Form.
| Prop | Type | Default | Description |
|---|---|---|---|
| resolverrequired | Resolver | — | Integrates external validation (e.g., zodResolver). Enables schema-based validation with Zod, Yup, or Joi. |
| defaultValuesrequired | DefaultValues<TFieldValues> | — | Default values for form fields. Required for controlled components like Select, Checkbox, and Switch. |
| mode | "onSubmit" | "onBlur" | "onChange" | "onTouched" | "all" | "onSubmit" | Determines when validation is triggered. Use 'onChange' for real-time feedback or 'onBlur' for validate-on-leave. |
| reValidateMode | "onSubmit" | "onBlur" | "onChange" | "onChange" | When to re-validate after an error has been shown. Defaults to re-validating on every change. |
| criteriaMode | "firstError" | "all" | "firstError" | Whether to show only the first error or all errors for a field. |
| shouldFocusError | boolean | true | When true, automatically focuses the first field with an error after submission. |
FormField Props
Props for the FormField component that connects each input to the form.
| Prop | Type | Default | Description |
|---|---|---|---|
| controlrequired | Control<TFieldValues> | — | The control object from useForm. Connects the field to the form state. |
| namerequired | Path<TFieldValues> | — | The name of the field. Must match a key in your Zod schema / defaultValues. |
| renderrequired | ({ field, fieldState, formState }) => ReactElement | — | Render prop that provides field binding (value, onChange, onBlur, ref) and state (error, isDirty, isTouched). |
| rules | RegisterOptions | — | Inline validation rules. Typically unused when using a resolver (Zod) for schema-level validation. |
| defaultValue | TFieldValues[name] | — | Default value for this specific field. Overrides the form-level defaultValues. |