Forms/Form

Form

Building forms with React Hook Form and Zod validation.

Themed

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.

This is your public display name.

Contact Form

Multiple fields with Zod validation, description text, and error messages.

We will never share your email.

Enter your message above.

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

  • FormLabel is linked to FormControl via htmlFor / id automatically
  • FormDescription and FormMessage are linked via aria-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>

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.

PropTypeDefaultDescription
...propsrequiredUseFormReturn<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.

PropTypeDefaultDescription
controlrequiredControl<TFieldValues>The form control object from useForm().
namerequiredFieldPath<TFieldValues>Field name matching a key in the form schema.
renderrequired({ field, fieldState, formState }) => ReactNodeRender 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.

PropTypeDefaultDescription
classNamestringAdditional CSS classes applied to the wrapping div.
childrenrequiredReactNodeFormLabel, FormControl, FormDescription, and FormMessage components.

FormLabel

Accessible label that auto-associates with FormControl and turns text-destructive on error.

PropTypeDefaultDescription
classNamestringAdditional CSS classes. Automatically applies text-destructive on error.

FormControl

Slot wrapper that injects id, aria-describedby, and aria-invalid onto the child control.

PropTypeDefaultDescription
childrenrequiredReactNodeThe 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.

PropTypeDefaultDescription
classNamestringAdditional CSS classes applied to the helper text paragraph.
childrenReactNodeHelper 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.

PropTypeDefaultDescription
classNamestringAdditional CSS classes applied to the error message paragraph.
childrenReactNodeFallback content when no validation error exists. Error messages from the schema take priority.