# React Hook Form

Build forms with React Hook Form, Zod and the Field component.

This guide builds a form with [React Hook Form](https://react-hook-form.com),
validates it with a [Zod](https://zod.dev) schema and shows each field's error
with [Field](https://carbon.style/docs/components/field).

## Demo

The form has a title and a description. On submit it validates both and shows
the values it accepted.

```tsx
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const formSchema = z.object({
  title: z
    .string()
    .min(5, "Bug title must be at least 5 characters.")
    .max(32, "Bug title must be at most 32 characters."),
  description: z
    .string()
    .min(20, "Description must be at least 20 characters.")
    .max(100, "Description must be at most 100 characters."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "28rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfDemo() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { title: "", description: "" },
  })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Bug Report</CardTitle>
        <CardDescription>Help us improve by reporting bugs you encounter.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-demo" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <FieldGroup>
            <Controller
              name="title"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Bug Title</FieldLabel>
                  <Input
                    {...field}
                    id="form-rhf-demo-title"
                    placeholder="Login button not working on mobile"
                    autoComplete="off"
                  />
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
            <Controller
              name="description"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Description</FieldLabel>
                  <InputGroup>
                    <Input
                      {...field}
                      id="form-rhf-demo-description"
                      placeholder="I'm having an issue with the login button on mobile."
                      autoComplete="off"
                    />
                    <InputGroupAddon align="inline-end">
                      <InputGroupText>{field.value.length}/100</InputGroupText>
                    </InputGroupAddon>
                  </InputGroup>
                  <FieldDescription>
                    Include steps to reproduce, expected behavior, and what actually happened.
                  </FieldDescription>
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
          </FieldGroup>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="form-rhf-demo">
            Submit
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

> **Note**
>
> The demo has no browser validation, so the schema errors are the only ones you
> see. Add `required`, `minLength` and the other native attributes in production.
> They cost nothing and work without JavaScript.

## Installation

```bash
npm install react-hook-form @hookform/resolvers zod
```

The examples also use [Field](https://carbon.style/docs/components/field),
[Input](https://carbon.style/docs/components/input), [Checkbox](https://carbon.style/docs/components/checkbox),
[Button](https://carbon.style/docs/components/button) and [Card](https://carbon.style/docs/components/card).

## Approach

- `useForm` holds the form state.
- `Controller` connects one control to it and reports that control's state.
- `Field` lays out the label, control, description and error.
- `zodResolver` runs the schema and maps its issues onto the fields.

## Anatomy

Every field in this guide has the same shape.

```tsx
<Controller
  name="title"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field invalid={fieldState.invalid}>
      <FieldLabel>Bug Title</FieldLabel>
      <Input {...field} placeholder="Login button not working on mobile" autoComplete="off" />
      <FieldDescription>Provide a concise title for your bug report.</FieldDescription>
      <FieldError errors={[fieldState.error]} />
    </Field>
  )}
/>
```

Pass `fieldState.invalid` to `Field` once. The label, error and control inherit it. `FieldLabel` associates with the control automatically.
`FieldError` renders nothing while `fieldState.error` is undefined, so it needs
no condition around it.

## Form

### 1. Create a form schema

Define the shape of the form with Zod.

```tsx title="form.tsx"
import * as z from "zod"

const formSchema = z.object({
  title: z
    .string()
    .min(5, "Bug title must be at least 5 characters.")
    .max(32, "Bug title must be at most 32 characters."),
  description: z
    .string()
    .min(20, "Description must be at least 20 characters.")
    .max(100, "Description must be at most 100 characters."),
})
```

### 2. Set up the form

Create the form with `useForm` and pass it the schema through `zodResolver`.
Every field needs a default value, or its control starts uncontrolled and
switches later.

```tsx title="form.tsx"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"

type FormValues = z.infer<typeof formSchema>

export function BugReportForm() {
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { title: "", description: "" },
  })

  function onSubmit(data: FormValues) {
    console.log(data)
  }

  return <form onSubmit={form.handleSubmit(onSubmit)}>{/* Build the form here */}</form>
}
```

### 3. Build the form

Add one `Controller` per field, each rendering a `Field`.

```tsx title="form.tsx"
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupText } from "@/components/ui/input-group"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const formSchema = z.object({
  title: z
    .string()
    .min(5, "Bug title must be at least 5 characters.")
    .max(32, "Bug title must be at most 32 characters."),
  description: z
    .string()
    .min(20, "Description must be at least 20 characters.")
    .max(100, "Description must be at most 100 characters."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "28rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfDemo() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { title: "", description: "" },
  })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Bug Report</CardTitle>
        <CardDescription>Help us improve by reporting bugs you encounter.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-demo" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <FieldGroup>
            <Controller
              name="title"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Bug Title</FieldLabel>
                  <Input
                    {...field}
                    id="form-rhf-demo-title"
                    placeholder="Login button not working on mobile"
                    autoComplete="off"
                  />
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
            <Controller
              name="description"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Description</FieldLabel>
                  <InputGroup>
                    <Input
                      {...field}
                      id="form-rhf-demo-description"
                      placeholder="I'm having an issue with the login button on mobile."
                      autoComplete="off"
                    />
                    <InputGroupAddon align="inline-end">
                      <InputGroupText>{field.value.length}/100</InputGroupText>
                    </InputGroupAddon>
                  </InputGroup>
                  <FieldDescription>
                    Include steps to reproduce, expected behavior, and what actually happened.
                  </FieldDescription>
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
          </FieldGroup>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="form-rhf-demo">
            Submit
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

### 4. Done

`handleSubmit` calls `onSubmit` with the parsed values only when the schema
passes. When it fails, React Hook Form marks the fields and `FieldError` shows
each message.

## Validation

### Client-side validation

Pass the schema to the `resolver` option. Zod's messages become the
`fieldState.error` each `Controller` receives.

```tsx title="example-form.tsx"
const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  defaultValues: { title: "", description: "" },
})
```

### Validation modes

Use `mode` to choose when validation runs.

```tsx title="form.tsx"
const form = useForm<z.infer<typeof formSchema>>({
  resolver: zodResolver(formSchema),
  mode: "onChange",
})
```

| Mode          | Description                                              |
| ------------- | -------------------------------------------------------- |
| `"onChange"`  | Validation triggers on every change.                     |
| `"onBlur"`    | Validation triggers on blur.                             |
| `"onSubmit"`  | Validation triggers on submit (default).                 |
| `"onTouched"` | Validation triggers on first blur, then on every change. |
| `"all"`       | Validation triggers on blur and change.                  |

## Displaying errors

Render `FieldError` with `errors={[fieldState.error]}` next to the control.
Set `invalid` on `Field`. The control inherits it.

```tsx title="form.tsx"
<Controller
  name="email"
  control={form.control}
  render={({ field, fieldState }) => (
    <Field invalid={fieldState.invalid}>
      <FieldLabel>Email</FieldLabel>
      <Input {...field} id={field.name} type="email" />
      <FieldError errors={[fieldState.error]} />
    </Field>
  )}
/>
```

## Working with different field types

Use the same `Field` wrapper for Textarea, Select, RadioGroup and Switch. Connect `field.value` and `field.onChange` to each control's value and change props.

### Input

Spread `field` onto the `Input`. It carries `name`, `value`, `onChange`,
`onBlur` and `ref`.

```tsx
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Field, FieldDescription, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const formSchema = z.object({
  username: z
    .string()
    .min(3, "Username must be at least 3 characters.")
    .max(10, "Username must be at most 10 characters.")
    .regex(/^[a-zA-Z0-9_]+$/, "Username can only contain letters, numbers, and underscores."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "28rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfInput() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { username: "" },
  })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Profile Settings</CardTitle>
        <CardDescription>Update your profile information below.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-input" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <FieldGroup>
            <Controller
              name="username"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Username</FieldLabel>
                  <Input
                    {...field}
                    id="form-rhf-input-username"
                    placeholder="shadcn"
                    autoComplete="username"
                  />
                  <FieldDescription>
                    This is your public display name. Must be between 3 and 10 characters. Must only
                    contain letters, numbers, and underscores.
                  </FieldDescription>
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
          </FieldGroup>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="form-rhf-input">
            Save
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

### Checkbox

A checkbox has no `value` string, so wire it by hand. Set `checked` from
`field.value` and pass `field.onChange` to `onCheckedChange`. For a set of
checkboxes, the value is an array and each box adds or removes its id.

```tsx
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const tasks = [
  { id: "push", label: "Push notifications" },
  { id: "email", label: "Email notifications" },
]

const formSchema = z.object({
  responses: z.boolean(),
  tasks: z.array(z.string()).min(1, "Please select at least one notification type."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "28rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfCheckbox() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { responses: true, tasks: [] },
  })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Notifications</CardTitle>
        <CardDescription>Manage your notification preferences.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-checkbox" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <FieldGroup>
            <Controller
              name="responses"
              control={form.control}
              render={({ field }) => (
                <Fieldset>
                  <FieldsetLegend variant="label">Responses</FieldsetLegend>
                  <FieldDescription>
                    Get notified for requests that take time, like research or image generation.
                  </FieldDescription>
                  <FieldGroup gap={3}>
                    <Field orientation="horizontal" disabled>
                      <Checkbox
                        id="form-rhf-checkbox-responses"
                        name={field.name}
                        checked={field.value}
                        onCheckedChange={field.onChange}
                      />
                      <FieldLabel>Push notifications</FieldLabel>
                    </Field>
                  </FieldGroup>
                </Fieldset>
              )}
            />
            <FieldSeparator />
            <Controller
              name="tasks"
              control={form.control}
              render={({ field, fieldState }) => (
                <Fieldset>
                  <FieldsetLegend variant="label">Tasks</FieldsetLegend>
                  <FieldDescription>
                    Get notified when tasks you&apos;ve created have updates.
                  </FieldDescription>
                  <FieldGroup gap={3}>
                    {tasks.map((task) => (
                      <Field key={task.id} orientation="horizontal" invalid={fieldState.invalid}>
                        <Checkbox
                          id={`form-rhf-checkbox-${task.id}`}
                          name={field.name}
                          checked={field.value.includes(task.id)}
                          onCheckedChange={(checked) =>
                            field.onChange(
                              checked
                                ? [...field.value, task.id]
                                : field.value.filter((value) => value !== task.id),
                            )
                          }
                        />
                        <FieldLabel>{task.label}</FieldLabel>
                      </Field>
                    ))}
                  </FieldGroup>
                  <FieldError errors={[fieldState.error]} />
                </Fieldset>
              )}
            />
          </FieldGroup>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="form-rhf-checkbox">
            Save
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

```tsx title="form.tsx"
<Controller
  name="tasks"
  control={form.control}
  render={({ field, fieldState }) => (
    <Fieldset>
      <FieldsetLegend variant="label">Tasks</FieldsetLegend>
      <FieldDescription>Get notified when tasks you've created have updates.</FieldDescription>
      <FieldGroup gap={3}>
        {tasks.map((task) => (
          <Field key={task.id} orientation="horizontal" invalid={fieldState.invalid}>
            <Checkbox
              id={`form-rhf-checkbox-${task.id}`}
              name={field.name}
              checked={field.value.includes(task.id)}
              onCheckedChange={(checked) =>
                field.onChange(
                  checked
                    ? [...field.value, task.id]
                    : field.value.filter((value) => value !== task.id),
                )
              }
            />
            <FieldLabel>{task.label}</FieldLabel>
          </Field>
        ))}
      </FieldGroup>
      <FieldError errors={[fieldState.error]} />
    </Fieldset>
  )}
/>
```

The error belongs to the set, not to one box, so render it after the
`FieldGroup` inside the `Fieldset`.

### Complex forms

This form has a choice-card set, an email field and a required agreement. The
agreement's error renders inside `FieldContent`, under its description.

```tsx
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { Controller, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
  FieldTitle,
} from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const addons = [
  { id: "analytics", title: "Analytics", description: "Advanced analytics and reporting" },
  { id: "backup", title: "Backup", description: "Automated daily backups" },
  { id: "support", title: "Priority Support", description: "24/7 premium customer support" },
]

const formSchema = z.object({
  addons: z
    .array(z.string())
    .min(1, "Please select at least one add-on")
    .max(2, "You can select up to 2 add-ons"),
  billingEmail: z.email("Enter a valid email address."),
  terms: z.boolean().refine((value) => value, "You must accept the terms to continue."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "24rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfComplex() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { addons: [], billingEmail: "", terms: false },
  })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>You&apos;re almost there!</CardTitle>
        <CardDescription>Choose your add-ons and billing details.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-complex" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <FieldGroup>
            <Controller
              name="addons"
              control={form.control}
              render={({ field, fieldState }) => (
                <Fieldset>
                  <FieldsetLegend variant="label">Add-ons</FieldsetLegend>
                  <FieldDescription>
                    Select additional features you&apos;d like to include.
                  </FieldDescription>
                  <FieldGroup gap={3}>
                    {addons.map((addon) => {
                      const checked = field.value.includes(addon.id)
                      return (
                        <FieldLabel
                          key={addon.id}
                          htmlFor={`form-rhf-complex-${addon.id}`}
                          checked={checked}
                        >
                          <Field orientation="horizontal" invalid={fieldState.invalid}>
                            <FieldContent>
                              <FieldTitle>{addon.title}</FieldTitle>
                              <FieldDescription>{addon.description}</FieldDescription>
                            </FieldContent>
                            <Checkbox
                              id={`form-rhf-complex-${addon.id}`}
                              name={field.name}
                              checked={checked}
                              onCheckedChange={(next) => {
                                field.onChange(
                                  next
                                    ? [...field.value, addon.id]
                                    : field.value.filter((value) => value !== addon.id),
                                )
                                field.onBlur()
                              }}
                            />
                          </Field>
                        </FieldLabel>
                      )
                    })}
                  </FieldGroup>
                  <FieldError errors={[fieldState.error]} />
                </Fieldset>
              )}
            />
            <FieldSeparator />
            <Controller
              name="billingEmail"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field invalid={fieldState.invalid}>
                  <FieldLabel>Billing Email</FieldLabel>
                  <Input
                    {...field}
                    id="form-rhf-complex-billing-email"
                    type="email"
                    placeholder="billing@example.com"
                    autoComplete="email"
                  />
                  <FieldDescription>Receipts and invoices go here.</FieldDescription>
                  <FieldError errors={[fieldState.error]} />
                </Field>
              )}
            />
            <FieldSeparator />
            <Controller
              name="terms"
              control={form.control}
              render={({ field, fieldState }) => (
                <Field orientation="horizontal" invalid={fieldState.invalid}>
                  <Checkbox
                    id="form-rhf-complex-terms"
                    name={field.name}
                    checked={field.value}
                    onCheckedChange={field.onChange}
                  />
                  <FieldContent>
                    <FieldLabel>Accept the terms</FieldLabel>
                    <FieldDescription>
                      You agree to the terms of service and the privacy policy.
                    </FieldDescription>
                    <FieldError errors={[fieldState.error]} />
                  </FieldContent>
                </Field>
              )}
            />
          </FieldGroup>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field>
          <Button type="submit" form="form-rhf-complex">
            Save Preferences
          </Button>
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

## Resetting the form

Use `form.reset()` to return every field to its default value.

```tsx
<Button type="button" variant="outline" onClick={() => form.reset()}>
  Reset
</Button>
```

## Array fields

`useFieldArray` manages a list of fields the user can add to and remove from.

```tsx
"use client"

import { useState } from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import * as stylex from "@stylexjs/stylex"
import { X } from "lucide-react"
import { Controller, useFieldArray, useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
  Card,
  CardContent,
  CardDescription,
  CardFooter,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
import { Field, FieldContent, FieldDescription, FieldError, FieldGroup } from "@/components/ui/field"
import { FieldsetLegend, Fieldset } from "@/components/ui/fieldset"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupButton } from "@/components/ui/input-group"
import { color, font, leading, radius, space, text } from "@/theme/tokens.stylex"

const formSchema = z.object({
  emails: z
    .array(z.object({ address: z.email("Enter a valid email address.") }))
    .min(1, "Add at least one email address.")
    .max(5, "You can add up to 5 email addresses."),
})

type FormValues = z.infer<typeof formSchema>

const styles = stylex.create({
  card: { width: "100%", maxWidth: "28rem" },
  output: {
    marginBlockStart: space.s4,
    overflowX: "auto",
    borderRadius: radius.md,
    backgroundColor: color.muted,
    padding: space.s3,
    fontFamily: font.mono,
    fontSize: text.xs,
    lineHeight: leading.xs,
  },
})

export default function FormRhfArray() {
  const [submitted, setSubmitted] = useState<FormValues | null>(null)
  const form = useForm<FormValues>({
    resolver: zodResolver(formSchema),
    defaultValues: { emails: [{ address: "" }, { address: "" }] },
  })
  const { fields, append, remove } = useFieldArray({ control: form.control, name: "emails" })

  return (
    <Card xstyle={styles.card}>
      <CardHeader>
        <CardTitle>Contact Emails</CardTitle>
        <CardDescription>Manage your contact email addresses.</CardDescription>
      </CardHeader>
      <CardContent>
        <form id="form-rhf-array" onSubmit={form.handleSubmit((data) => setSubmitted(data))}>
          <Fieldset>
            <FieldsetLegend variant="label">Email Addresses</FieldsetLegend>
            <FieldDescription>
              Add up to 5 email addresses where we can contact you.
            </FieldDescription>
            <FieldGroup gap={4}>
              {fields.map((item, index) => (
                <Controller
                  key={item.id}
                  name={`emails.${index}.address`}
                  control={form.control}
                  render={({ field, fieldState }) => (
                    <Field orientation="horizontal" invalid={fieldState.invalid}>
                      <FieldContent>
                        <InputGroup>
                          <Input
                            {...field}
                            id={`form-rhf-array-email-${index}`}
                            aria-label={`Email ${index + 1}`}
                            placeholder="name@example.com"
                            type="email"
                            autoComplete="email"
                          />
                          {fields.length > 1 && (
                            <InputGroupAddon align="inline-end">
                              <InputGroupButton
                                type="button"
                                variant="ghost"
                                size="icon-xs"
                                onClick={() => remove(index)}
                                aria-label={`Remove email ${index + 1}`}
                              >
                                <X />
                              </InputGroupButton>
                            </InputGroupAddon>
                          )}
                        </InputGroup>
                        <FieldError errors={[fieldState.error]} />
                      </FieldContent>
                    </Field>
                  )}
                />
              ))}
              <Field orientation="horizontal">
                <Button
                  type="button"
                  variant="outline"
                  size="sm"
                  onClick={() => append({ address: "" })}
                  disabled={fields.length >= 5}
                >
                  Add Email Address
                </Button>
              </Field>
            </FieldGroup>
          </Fieldset>
        </form>
        {submitted && (
          <pre {...stylex.props(styles.output)}>{JSON.stringify(submitted, null, 2)}</pre>
        )}
      </CardContent>
      <CardFooter>
        <Field orientation="horizontal">
          <Button
            type="button"
            variant="outline"
            onClick={() => {
              form.reset()
              setSubmitted(null)
            }}
          >
            Reset
          </Button>
          <Button type="submit" form="form-rhf-array">
            Save
          </Button>
        </Field>
      </CardFooter>
    </Card>
  )
}
```

### Using useFieldArray

It returns the current `fields` plus `append` and `remove`.

```tsx title="form.tsx"
import { useFieldArray, useForm } from "react-hook-form"

export function ExampleForm() {
  const form = useForm({
    // ... form config
  })

  const { fields, append, remove } = useFieldArray({
    control: form.control,
    name: "emails",
  })
}
```

### Array field structure

Wrap the list in a `Fieldset` with a `FieldsetLegend` and `FieldDescription`.

```tsx title="form.tsx"
<Fieldset>
  <FieldsetLegend variant="label">Email Addresses</FieldsetLegend>
  <FieldDescription>Add up to 5 email addresses where we can contact you.</FieldDescription>
  <FieldGroup gap={4}>{/* Array items go here */}</FieldGroup>
</Fieldset>
```

### Controller pattern for array items

Map over `fields` with one `Controller` per item. Key each one by `field.id`,
not by index, or a removed row keeps the next row's state.

```tsx title="form.tsx"
{
  fields.map((item, index) => (
    <Controller
      key={item.id}
      name={`emails.${index}.address`}
      control={form.control}
      render={({ field, fieldState }) => (
        <Field orientation="horizontal" invalid={fieldState.invalid}>
          <FieldContent>
            <InputGroup invalid={fieldState.invalid}>
              <Input
                {...field}
                id={`form-rhf-array-email-${index}`}
                aria-label={`Email ${index + 1}`}
                placeholder="name@example.com"
                type="email"
                autoComplete="email"
              />
              {/* Remove button */}
            </InputGroup>
            <FieldError errors={[fieldState.error]} />
          </FieldContent>
        </Field>
      )}
    />
  ))
}
```

### Adding items

Call `append` with a new item's default value.

```tsx title="form.tsx"
<Button
  type="button"
  variant="outline"
  size="sm"
  onClick={() => append({ address: "" })}
  disabled={fields.length >= 5}
>
  Add Email Address
</Button>
```

### Removing items

Call `remove` with the index. Render the button only while more than one row
remains, so the list cannot be emptied from the UI.

```tsx title="form.tsx"
{
  fields.length > 1 && (
    <InputGroupAddon align="inline-end">
      <InputGroupButton
        type="button"
        variant="ghost"
        size="icon-xs"
        onClick={() => remove(index)}
        aria-label={`Remove email ${index + 1}`}
      >
        <X />
      </InputGroupButton>
    </InputGroupAddon>
  )
}
```

### Array validation

Validate the list with `z.array`. A length rule on the array reports against
the array itself, so render its error after the `FieldGroup`.

```tsx title="form.tsx"
const formSchema = z.object({
  emails: z
    .array(z.object({ address: z.email("Enter a valid email address.") }))
    .min(1, "Add at least one email address.")
    .max(5, "You can add up to 5 email addresses."),
})
```

## Base UI Form

Use [Form](https://carbon.style/docs/components/form) for native validation and server errors without React Hook Form. Pass server messages through `errors`, keyed by `Field` name. Editing a field clears its server error.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Form } from "@/components/ui/form"
import { Field, FieldError, FieldGroup, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { Button } from "@/components/ui/button"

const styles = stylex.create({ form: { width: "20rem" } })

export default function FormDemo() {
  const [errors, setErrors] = useState<Record<string, string>>({})
  const [submitted, setSubmitted] = useState(false)
  return (
    <Form
      errors={errors}
      xstyle={styles.form}
      onFormSubmit={(values) => {
        if (values.username === "admin") {
          setErrors({ username: "This username is taken." })
          setSubmitted(false)
        } else {
          setErrors({})
          setSubmitted(true)
        }
      }}
    >
      <FieldGroup>
        <Field name="username">
          <FieldLabel>Username</FieldLabel>
          <Input required placeholder="Try admin" />
          <FieldError />
        </Field>
        <Button type="submit">Save</Button>
        {submitted && <output>Profile saved.</output>}
      </FieldGroup>
    </Form>
  )
}
```
