# How to Integrate shadcn UI Components with React Hook Form and Zod: A Complete Guide

> Learn how to integrate shadcn UI components with React Hook Form and Zod for seamless form validation. Build robust forms with our complete guide.

- Repository: [shadcn-ui/ui](https://github.com/shadcn-ui/ui)
- Tags: how-to-guide
- Published: 2026-02-26

---

**shadcn/ui provides a lightweight Form abstraction built on top of react-hook-form that connects UI components like Input, Textarea, and Select to validation libraries through composable primitives including FormField, FormItem, and FormMessage.**

The shadcn/ui repository ships with a type-safe form system that bridges the gap between accessible UI components and robust validation. By leveraging react-hook-form as the foundation and optionally adding Zod for schema validation, you can build complex forms with minimal boilerplate while maintaining full TypeScript inference.

## Understanding the shadcn UI Form Architecture

The form system is implemented in [`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx) and consists of thin wrappers around react-hook-form's API. These wrappers handle accessibility attributes, error messaging, and component composition while preserving the full power of the underlying library.

### Core Primitives and Their Roles

The Form abstraction exports seven key components:

- **Form**: An alias for `FormProvider` from react-hook-form that exposes `register`, `control`, and `handleSubmit` to child components.
- **FormField**: Uses `Controller` to connect a specific field name to the form state and stores the name in `FormFieldContext`.
- **FormItem**: Generates a unique ID via `React.useId()` and provides it to child components through `FormItemContext`.
- **FormLabel**: Associates the label with the input using the generated ID.
- **FormControl**: Forwards the ID and ARIA attributes (`aria-invalid`, `aria-describedby`) to the actual UI component.
- **FormDescription**: Provides supplementary text linked via `aria-describedby`.
- **FormMessage**: Displays validation errors from the field state.

### How FormField Connects to react-hook-form

The `FormField` component in [`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx) (lines 32-43) implements the bridge between your UI and react-hook-form:

```typescript
// Simplified structure from the source
<Controller
  name={name}
  control={control}
  render={({ field, fieldState }) => (
    <FormFieldContext.Provider value={{ name, fieldState }}>
      {children}
    </FormFieldContext.Provider>
  )}
/>

```

This pattern ensures that any component inside `FormField` can access the field's state through the `useFormField` hook, which reads both `FormFieldContext` and `FormItemContext` (lines 45-66 in the same file).

## Setting Up Zod Validation with shadcn UI Forms

To add schema validation, install the Zod resolver and define your schema. The `zodResolver` from `@hookform/resolvers/zod` integrates seamlessly with the shadcn form primitives.

```bash
npm install zod @hookform/resolvers/zod

```

The following example from [`deprecated/www/registry/new-york/examples/textarea-form.tsx`](https://github.com/shadcn-ui/ui/blob/main/deprecated/www/registry/new-york/examples/textarea-form.tsx) demonstrates a complete implementation:

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

import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
} from "@/registry/new-york/ui/form"
import { Textarea } from "@/registry/new-york/ui/textarea"
import { Button } from "@/registry/new-york/ui/button"

const schema = z.object({
  bio: z
    .string()
    .min(10, "Bio must be at least 10 characters")
    .max(160, "Bio must be ≤ 160 characters"),
})

export function BioForm() {
  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
  })

  function onSubmit(data: z.infer<typeof schema>) {
    console.log("Submitted:", data)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <FormField
          control={form.control}
          name="bio"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Bio</FormLabel>
              <FormControl>
                <Textarea placeholder="Tell us about yourself" {...field} />
              </FormControl>
              <FormDescription>
                Write a short paragraph (10-160 characters).
              </FormDescription>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Save</Button>
      </form>
    </Form>
  )
}

```

In this implementation, `FormMessage` automatically displays errors generated by Zod when validation fails, while `FormDescription` provides context to assistive technologies via `aria-describedby`.

## Working with Different Input Types

The shadcn form system is component-agnostic. You can wrap any input component that accepts `value` and `onChange` (or `onValueChange`) props.

### Text Inputs and Textareas

Standard text inputs from [`apps/v4/registry/new-york-v4/ui/input.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/input.tsx) work immediately when placed inside `FormControl`:

```tsx
<FormControl>
  <Input type="email" placeholder="you@example.com" {...field} />
</FormControl>

```

The `FormControl` component automatically injects the `id`, `aria-invalid` (when errors exist), and `aria-describedby` (linking to description and error messages) attributes.

### Select Components with Enum Validation

For select inputs, use the `Select` component from the registry. The following pattern validates against a Zod enum:

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

import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormMessage,
} from "@/registry/new-york/ui/form"
import { Select } from "@/registry/new-york/ui/select"
import { Button } from "@/registry/new-york/ui/button"

const schema = z.object({
  role: z.enum(["admin", "editor", "viewer"], {
    errorMap: () => ({ message: "Select a valid role" }),
  }),
})

export function RoleForm() {
  const form = useForm<z.infer<typeof schema>>({
    resolver: zodResolver(schema),
  })

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
        <FormField
          control={form.control}
          name="role"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Role</FormLabel>
              <FormControl>
                <Select
                  value={field.value}
                  onValueChange={field.onChange}
                  placeholder="Choose a role"
                >
                  <Select.Item value="admin">Admin</Select.Item>
                  <Select.Item value="editor">Editor</Select.Item>
                  <Select.Item value="viewer">Viewer</Select.Item>
                </Select>
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
        <Button type="submit">Submit</Button>
      </form>
    </Form>
  )
}

```

The `Select` component's `value` and `onValueChange` props map directly to the `Controller`-provided `field` object, ensuring type-safe two-way binding.

## Key Implementation Files in the shadcn/ui Repository

Understanding the source structure helps when debugging or extending the form system:

- **[`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx)**: Contains the core primitives (`Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`) and the `useFormField` hook. This is where `FormProvider` and `Controller` are wrapped.
- **[`apps/v4/registry/new-york-v4/ui/input.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/input.tsx)**: Standard text input component that receives forwarded refs and ARIA attributes from `FormControl`.
- **[`apps/v4/registry/new-york-v4/ui/textarea.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/textarea.tsx)**: Multi-line text input implementation.
- **[`deprecated/www/registry/new-york/examples/textarea-form.tsx`](https://github.com/shadcn-ui/ui/blob/main/deprecated/www/registry/new-york/examples/textarea-form.tsx)**: Complete working example demonstrating Zod integration with the form primitives.
- **[`apps/v4/registry/new-york-v4/ui/select.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/select.tsx)** (or equivalent registry path): Select component implementation that accepts `value` and `onValueChange` props for Controller compatibility.

These files constitute the bridge between shadcn UI's visual components and the robust validation capabilities of React Hook Form and Zod.

## Summary

- **shadcn/ui** provides a thin, type-safe abstraction over **react-hook-form** through composable primitives like `FormField`, `FormItem`, and `FormMessage`.
- The `Form` component acts as a `FormProvider`, while `FormField` uses `Controller` to register fields and expose state to UI components.
- **Zod** integration requires only the `zodResolver` from `@hookform/resolvers/zod`; validation errors automatically surface in `FormMessage` components.
- All primitives are unopinionated about the specific UI component used, allowing seamless swapping of `Input`, `Textarea`, `Select`, or custom components.
- Source files in [`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx) implement the context wiring and accessibility attributes (`aria-invalid`, `aria-describedby`) that make forms accessible by default.

## Frequently Asked Questions

### Do I need to use Zod with shadcn/ui forms?

No. While Zod provides schema-based validation, the shadcn form primitives work with any validation strategy supported by react-hook-form. You can use Yup, Joi, custom validation functions, or even native HTML5 validation attributes. The `FormMessage` component will display any error object returned by your chosen resolver.

### How does the Form component handle accessibility?

The form system automatically manages ARIA attributes through the `useFormField` hook implemented in [`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx). When a validation error occurs, `FormControl` sets `aria-invalid="true"` on the input. `FormDescription` and `FormMessage` generate unique IDs that are referenced by `aria-describedby`, ensuring screen readers announce both helper text and error messages.

### Can I use shadcn form primitives with custom components?

Yes. The `FormField` component uses react-hook-form's `Controller`, which only requires that your custom component accepts `value` and `onChange` (or `onValueChange`) props. Wrap your component inside `FormControl` to automatically receive the generated ID and ARIA attributes. This pattern works for complex components like date pickers, rich text editors, or custom select implementations.

### Where are the form primitives located in the shadcn/ui codebase?

The core form primitives reside in [`apps/v4/registry/new-york-v4/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/new-york-v4/ui/form.tsx) for the v4 registry, with legacy versions available in [`deprecated/www/registry/new-york/ui/form.tsx`](https://github.com/shadcn-ui/ui/blob/main/deprecated/www/registry/new-york/ui/form.tsx). The `useFormField` hook and all context providers (`FormFieldContext`, `FormItemContext`) are defined in these files, making them the authoritative source for understanding how the abstraction layer works.