Form
Building forms with React Hook Form and Zod validation.
When to Use
Use Form when you need to:
- Collect and validate user input across one or more fields
- Display inline validation errors alongside each control
- Integrate schema-based validation (Zod, Yup, etc.) with React Hook Form
- Maintain accessible label, description, and error associations automatically
- Handle complex multi-step or multi-section forms with consistent structure
When Not to Use
- Single standalone input without validation — use Field for simpler composition
- Server-only forms without client-side validation — use a plain
<form>with server actions - Search bars or single-field filters — use Input directly
- TanStack-based projects — use TanStack Form integration instead
Profile Form
A single-field form with username validation and helper text.
Contact Form
Multiple fields with Zod validation, description text, and error messages.
Anatomy
The composable structure of a Form with FormField. Each field wraps a label, control, description, and error message.
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<FormField
control={form.control}
name="fieldName"
render={({ field }) => (
<FormItem>
<FormLabel />
<FormControl>
{/* Input, Select, Textarea, etc. */}
</FormControl>
<FormDescription />
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>UX & Design Guidelines
Field Layout
Stack fields vertically with space-y-6 for clear separation. Group related fields (e.g. first name / last name) side-by-side using grid grid-cols-2 gap-4. Place the submit button outside the field stack with extra top margin.
Validation Feedback
Display errors inline below each field using FormMessage. Validate on blur for long forms and on submit for short forms. Use FormDescription to provide format hints (e.g. "Must be at least 8 characters") before the user encounters an error.
Responsive Behavior
Use max-w-sm or max-w-md to constrain form width for readability. On mobile, all side-by-side fields should collapse to a single column. Submit buttons should stretch to full width on small screens with className="w-full sm:w-auto".
Color & Contrast
Error text uses text-destructive which meets WCAG 2.1 AA contrast. Labels use text-foreground and descriptions use text-muted-foreground for clear visual hierarchy. Never use color alone to indicate errors — FormMessage provides descriptive text alongside the color change.
Accessibility
Keyboard Navigation
- Tab — Move focus between form controls in source order
- Shift + Tab — Move focus to the previous control
- Enter — Submit the form when a submit button or input is focused
- Space — Toggle checkboxes and switches within the form
Screen Reader Support
FormLabelis linked toFormControlviahtmlFor/idautomaticallyFormDescriptionandFormMessageare linked viaaria-describedby- Error states set
aria-invalid="true"on the control so assistive tech announces invalid fields - Unique IDs are generated with
React.useId()to prevent collisions
Focus Management
After a failed submission, focus the first field with an error to guide the user. Use form.setFocus("fieldName") from react-hook-form. Visible focus rings follow the global focus-visible:ring-[3px] convention and meet WCAG 2.1 Level AA requirements.
Error Announcements
FormMessage renders error text linked to the input via aria-describedby, ensuring screen readers announce validation errors when they appear. The data-error attribute on labels provides a visual cue while the ARIA relationship provides the programmatic one.
ARIA Attributes
{/* FormLabel automatically associates with FormControl via htmlFor */}
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" {...field} />
</FormControl>
{/* aria-describedby links description + error to the input */}
<FormDescription>Your primary email address.</FormDescription>
<FormMessage /> {/* aria-invalid applied when errors exist */}
</FormItem>
{/* Disabled field */}
<FormItem>
<FormLabel>Username</FormLabel>
<FormControl>
<Input disabled aria-disabled="true" {...field} />
</FormControl>
<FormDescription>Contact support to change your username.</FormDescription>
</FormItem>Related Components
Field
Simpler composition API for label, description, and error wrapping without react-hook-form.
Label
Standalone accessible label component built on Radix UI primitives.
React Hook Form
Integration patterns for React Hook Form with all input types, selects, and checkboxes.
TanStack Form
Alternative form library integration with field-level and async validation support.
API Reference
The Form system is composed of several sub-components. Each one is documented below.
Form
Provider wrapper that spreads UseFormReturn into react-hook-form's FormProvider.
| Prop | Type | Default | Description |
|---|---|---|---|
| ...propsrequired | UseFormReturn<TFieldValues> | — | Spread the return value of useForm(). Provides form context to all child FormField components. |
FormField
Controlled field component wrapping react-hook-form's Controller.
| Prop | Type | Default | Description |
|---|---|---|---|
| controlrequired | Control<TFieldValues> | — | The form control object from useForm(). |
| namerequired | FieldPath<TFieldValues> | — | Field name matching a key in the form schema. |
| renderrequired | ({ field, fieldState, formState }) => ReactNode | — | Render function that receives field props, field state, and form state. |
FormItem
Container for a single field. Generates a unique ID via React.useId() for ARIA associations.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional CSS classes applied to the wrapping div. |
| childrenrequired | ReactNode | — | FormLabel, FormControl, FormDescription, and FormMessage components. |
FormLabel
Accessible label that auto-associates with FormControl and turns text-destructive on error.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional CSS classes. Automatically applies text-destructive on error. |
FormControl
Slot wrapper that injects id, aria-describedby, and aria-invalid onto the child control.
| Prop | Type | Default | Description |
|---|---|---|---|
| childrenrequired | ReactNode | — | The form control element (Input, Select, Textarea, etc.). Receives id, aria-describedby, and aria-invalid automatically. |
FormDescription
Helper text paragraph linked to the control via aria-describedby.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional CSS classes applied to the helper text paragraph. |
| children | ReactNode | — | Helper text content displayed below the control. |
FormMessage
Displays validation error text from the schema, or falls back to children. Renders nothing when there is no error and no children.
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional CSS classes applied to the error message paragraph. |
| children | ReactNode | — | Fallback content when no validation error exists. Error messages from the schema take priority. |