---
title: Form
description: Building forms with React Hook Form and Zod.
icon: file-text
author:
  name: shadcn
  url: https://github.com/shadcn
search:
  keywords: [react hook form, validation]
related:
  - /field
  - /input
  - /select
mode: wide
---

<PoweredBy
  packages={[
    { name: 'Base UI', url: 'https://base-ui.com/react/components/form' },
    { name: 'Shadcn UI', url: 'https://ui.shadcn.com/docs/components/form' },
    { name: 'react-hook-form', url: 'https://react-hook-form.com/' },
    { name: 'zod', url: 'https://zod.dev/' },
  ]}
/>

### Installation

```package-install
npx shadcn@latest add @redpanda/form
```

<ComponentPreview name="form-demo" />

## When to use

Use this decision tree to determine when to use the Form components:

```mermaid
graph TD;
    A[❓ Need user input collection?] --> B{What complexity level?};
    B --> C[✅ Multi-field forms];
    B --> D[✅ Complex validation needed];
    B --> E[ℹ️ Single input fields];
    B --> F[⚠️ No validation needed];

    C --> G[❓ Validation approach?];
    D --> G;
    E --> H[ℹ️ Use individual Input components];
    F --> I[⚠️ Use basic HTML forms];

    G --> J["✅ Schema-based (Zod)"];
    G --> K[ℹ️ Custom validation];

    J --> L[❓ Form complexity?];
    K --> M[ℹ️ Manual error handling];

    L --> N["✅ Complex forms (5+ fields)"];
    L --> O["✅ Simple forms (<5 fields)"];

    N --> P[✅ Use full Form composition];
    O --> Q[✅ Use SimpleFormField wrapper];

    P --> R[❓ Field types needed?];
    Q --> R;
    M --> R;

    R --> S[✅ Text inputs - use FormField + Input];
    R --> T[✅ Selections - use FormField + Select/Combobox];
    R --> U[✅ Dates - use FormField + DatePicker];
    R --> V[✅ Toggles - use FormField + Switch/Checkbox];

    S --> W[❓ Required field indicators?];
    T --> W;
    U --> W;
    V --> W;

    W --> X[✅ Use required field variants];
    W --> Y[ℹ️ Standard field labels];

    classDef recommended fill:#4ade80,stroke:#22c55e,stroke-width:2px,color:#000;
    classDef discouraged fill:#fbbf24,stroke:#f59e0b,stroke-width:2px,color:#000;
    classDef decision fill:#60a5fa,stroke:#3b82f6,stroke-width:2px,color:#fff;
    classDef neutral fill:#9ca3af,stroke:#6b7280,stroke-width:2px,color:#fff;

    class C,D,J,N,O,P,Q,S,T,U,V,X recommended;
    class F,I discouraged;
    class A,B,G,L,R,W decision;
    class E,H,K,M,Y neutral;
```

## Usage

### Create a form schema

Define the shape of your form using a Zod schema. You can read more about using Zod in the [Zod documentation](https://zod.dev).

```tsx lineNumbers title="components/example-form.tsx" {3,5-7}
'use client';

import { z } from 'zod';

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

### Define a form

Use the `useForm` hook from `react-hook-form` to create a form.

```tsx lineNumbers title="components/example-form.tsx" {3-4,14-20,22-27}
'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](https://react-hook-form.com/docs/usecontroller) to learn more about controlled components.

### Build your form

We can now use the `<Form />` components to build our form.

```tsx lineNumbers title="components/example-form.tsx" {7-17,28-50} expandable
'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>
  );
}
```

### Done

That's it. You now have a fully accessible form that is type-safe with client-side validation.

<ComponentPreview name="input-form-demo" className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden" />

## Examples

### Simple Form

Use `SimpleFormField` for a single control such as Input or Textarea. For compound cards, option groups and date
popovers, compose `FormField`, `FormItem` and `FormControl` so the field's label, ref and validation attributes reach
the actual interactive control. Put `FormControl` around `PopoverTrigger`, not inside its render element: this lets
the field supply the trigger ID that its label targets.

Both profile examples share one schema and concrete profile email, notification, date-of-birth and email-preference
components. These live together in the profile example library; they are not generic field templates. Username and
Bio deliberately demonstrate manual `FormField` composition in the full example and `SimpleFormField` in the simple
example. Each option retains its own associated label, actual control ref and validation description.

The Bio field accepts 10–160 characters, and failed submission focuses the first invalid control. Use existing
`FormItem` layout and spacing recipes instead of local spacing overrides. Horizontal rows use the standard spacing
scale; `layout="card-horizontal"` owns switch-card alignment and padding.

<ComponentPreview name="form-simple-demo" className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden" />

### Required fields

<ComponentPreview name="form-required-demo" className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden" />

### JSON Field

<ComponentPreview name="json-field-form-demo" className="[&_[role=tablist]]:hidden [&>div>div:first-child]:hidden" />

See the following links for more examples on how to use the `<Form />` component with other components:

- [Checkbox](/docs/checkbox#form)
- [Date Picker](/docs/date-picker#date-picker-with-form)
- [Input](/docs/input#form)
- [JSON Field](/docs/json-field#form-integration)
- [Radio Group](/docs/radio-group#form)
- [Select](/docs/select#form)
- [Multi Select](/docs/multi-select#form)
- [Switch](/docs/switch#form)
- [Textarea](/docs/textarea#form)
- [Combobox](/docs/combobox#form-integration)
- [Field choice cards](/docs/field#choice-card)

## Credits

- We use [React Hook Form](https://react-hook-form.com/) for form state and Base UI's
  [useRender](https://base-ui.com/react/utils/use-render) for control composition.
- We take our inspiration from [Shadcn UI](https://ui.shadcn.com/docs/components/form) for the form style.
