Docs
React Hook Form

React Hook Form

A guide to constructing robust, accessible, and type-safe forms in React using React Hook Form for state management and Zod for schema validation, integrated with our UI components.

Forms represent a critical interface in web applications, yet they often introduce significant complexity due to state management, validation, accessibility, and styling requirements.

Effective HTML forms should ideally possess the following characteristics:

  • Semantic Structure: Utilize appropriate HTML elements (<form>, <label>, <input>, etc.) for inherent meaning and browser/assistive technology understanding.

  • Navigability: Ensure seamless keyboard navigation between fields using Tab and Shift+Tab.

  • Accessibility: Implement ARIA attributes where necessary and associate labels correctly with their corresponding inputs for screen reader compatibility.

  • Robust Validation: Include mechanisms for both client-side (immediate feedback) and server-side (authoritative) validation.

  • Consistent Styling: Maintain visual harmony with the overall application design system.

This guide focuses on leveraging the strengths of react-hook-form for efficient form state management and performance, combined with zod for powerful schema definition and validation. We introduce a set of composable <Form> components, including <FormField>, designed to bridge these libraries with our accessible Radix UI-based input components, simplifying the creation of high-quality forms.

Key Features of this Form Approach

Our <Form> component set acts as a helpful layer on top of react-hook-form, streamlining the integration with our UI library and Zod. It offers:

  • Composable Structure: Provides components like <Form>, <FormField>, <FormItem>, <FormLabel>, <FormControl>, <FormDescription>, and <FormMessage> for building forms declaratively.

  • Simplified Field Control: The <FormField> component abstracts the connection between react-hook-form's state and controlled input components.

  • Integrated Zod Validation: Seamlessly integrates Zod schemas for robust type-safe validation via the @hookform/resolvers library.

  • Enhanced Accessibility: Automatically handles ARIA attributes (like aria-describedby, aria-invalid) and associates labels with inputs using React.useId() for unique, stable IDs. Error messages are linked correctly.

  • Radix UI Compatibility: Designed to work smoothly with all Radix UI-based input components (Input, Select, Checkbox, etc.).

  • Schema Agnostic (BYOS): While Zod is used in examples, the underlying structure supports other schema validation libraries compatible with react-hook-form resolvers.

  • Full Markup Control: You retain complete control over the rendered HTML structure and styling, applying Tailwind CSS classes or custom styles as needed.

Form Component Anatomy

The core structure revolves around the <FormField> component, which connects a specific field in your react-hook-form state to the UI input elements.

<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)}>
    <FormField
      control={form.control}
      name="fieldName"
      render={({ field }) => (
        <FormItem>
          <FormLabel>Field Label</FormLabel>
          <FormControl>
            <Input placeholder="Enter value..." {...field} />
          </FormControl>
          <FormDescription>Provide additional context here.</FormDescription>
          <FormMessage />
        </FormItem>
      )}
    />
    {/* ... more FormField components ... */}
    <Button type="submit">Submit</Button>
  </form>
</Form>

This structure ensures proper accessibility linkages (label to input, input to description/error message) and provides clear containers for styling individual parts of a form field.

Basic Field Example

const form = useForm()

<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input placeholder="shadcn" {...field} />
      </FormControl>
      <FormDescription>This is your public display name.</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Form Component Installation

Command

npx shadcn@latest add form

Step-by-Step Form Implementation

Step 1: Define Your Form Schema with Zod

Begin by defining the structure and validation rules for your form data using a Zod schema. Zod provides a powerful and type-safe way to declare expected data types, required fields, string lengths, email formats, and more. Consult the Zod documentation for detailed schema creation options.

"use client";

const formSchema = z.object({
  username: z.string().min(2).max(50),
});

Step 2: Initialize the Form with useForm

In your React component, utilize the useForm hook provided by react-hook-form to initialize your form's state management. Configure it with your Zod schema using the zodResolver and provide defaultValues for your form fields.

"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
});

export function ProfileForm() {
  // 1. Define your form.
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  });

  // 2. Define a submit handler.
  function onSubmit(values: z.infer<typeof formSchema>) {
    // Do something with the form values.
    // ✅ This will be type-safe and validated.
    console.log(values);
  }
}

Since FormField is using a controlled component, you need to provide a default value for the field. See the React Hook Form docs to learn more about controlled components.

Step 3: Construct the Form UI

With the form state initialized, use the <Form> and <FormField> components to render the actual form elements. Connect the form object from useForm to the <Form> component and pass form.control to each <FormField>. Use the render prop pattern within <FormField> to structure your label, input, description, and error message using the provided sub-components.

"use client";

import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";

import { Button } from "@/components/ui/button";
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";

const formSchema = z.object({
  username: z.string().min(2, {
    message: "Username must be at least 2 characters.",
  }),
});

export function ProfileForm() {
  // ...

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8">
        <FormField
          control={form.control}
          name="username"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Username</FormLabel>
              <FormControl>
                <Input placeholder="shadcn" {...field} />
              </FormControl>
              <FormDescription>
                This is your public display name.
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  );
}

Step 4: Completion

Following these steps results in a fully functional, accessible, and type-safe form with integrated client-side validation powered by Zod and React Hook Form, rendered using our consistent UI components.

Loading...

Further Form Integration Examples

The <Form> component system is designed to work seamlessly with various input components. Explore these examples to see specific integrations:

    React Hook Form