# Shadcn Form

> Free shadcn/ui form component for React — built on react-hook-form + zod. Type-safe validation, accessible labels and error messages, and worked examples: login, sign-up, contact, checkboxes, switches, radio, date picker, and settings.

Source: https://designrevision.com/components/form

The **Shadcn Form** is the bridge between <a href="https://react-hook-form.com/" rel="nofollow">**react-hook-form**</a> and your shadcn inputs. It is a handful of composable parts that wire **labels, controls, descriptions, and validation errors** together — accessibly and type-safely — so a fully validated form is mostly schema and composition. The examples below are complete, copy-paste forms: login, sign-up, contact, checkboxes, switches, radio, a date picker, and settings.

## Built on react-hook-form + zod

The Form is **not an input** — it is structure. Three libraries do the work:

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

- **react-hook-form** manages field state, submission, and validation with minimal re-renders.
- **zod** declares your schema — the rules *and* the TypeScript types.
- **@hookform/resolvers** connects the two via `zodResolver`.

## Anatomy

Seven parts compose every field:

- **`Form`** — react-hook-form's `FormProvider`; wrap your `<form>` in it.
- **`FormField`** — binds one field by `name` to `form.control` and exposes a `field` render prop.
- **`FormItem`** — groups one field's label, control, description, and message.
- **`FormLabel`** — a label wired to the control (turns red on error).
- **`FormControl`** — a `Slot` that puts `id`, `aria-invalid`, and `aria-describedby` onto your input.
- **`FormDescription`** — helper text.
- **`FormMessage`** — renders the field's validation error automatically.

## A complete example

Define the schema, wire the resolver, compose the fields:

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

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

  function onSubmit(values: z.infer<typeof formSchema>) {
    // values is fully typed and validated
    console.log(values)
  }

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
        <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>
  )
}
```

There is **no manual error handling** — when `username` fails the schema, `FormMessage` shows the message and `FormLabel` turns red.

## Validation with zod

The schema is the single source of truth for both **rules** and **types**:

- **Per-field rules** carry their own message: `z.string().email({ message: "…" })`, `.min(8, { message: "…" })`, `.max(160)`.
- **Booleans that must be true** (accept-terms): `z.boolean().refine((v) => v === true, { message: "…" })`.
- **Enums** (radio / select): `z.enum(["all", "mentions", "none"])`.
- **Cross-field rules** use `.refine` on the object and target a field with `path`:

```tsx
const schema = z
  .object({ password: z.string().min(8), confirm: z.string() })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords do not match.",
    path: ["confirm"],
  })
```

The Sign-up example uses exactly this for password confirmation.

## Binding each field type

Inputs and textareas spread `{...field}`. Controlled components map their own props:

| Control | Binding |
|---|---|
| Input / Textarea | `{...field}` |
| Checkbox / Switch | `checked={field.value} onCheckedChange={field.onChange}` |
| Select | `value={field.value} onValueChange={field.onChange}` |
| RadioGroup | `value={field.value} onValueChange={field.onChange}` |
| Calendar (in a Popover) | `selected={field.value} onSelect={field.onChange}` |

The Contact, Checkbox, Switch, Radio, and Date examples each show one in a real form.

## Submitting

`form.handleSubmit(onSubmit)` runs validation first and only calls your handler with **typed, valid** values. From there you can call a **Next.js server action** or a route handler — and re-validate with the same zod schema on the server, since client validation is for UX and server validation is for trust.

## Accessibility

The wiring is automatic: `FormControl` links the input to its `FormDescription` and `FormMessage` through `aria-describedby`, sets `aria-invalid` when the field errors, and `FormLabel` is associated via `htmlFor` and turns red on error. Errors are therefore both **visible and announced** with no extra markup.

## Installation

### form

**Install:**

```bash
npx shadcn@latest add @designrevision/form
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, @radix-ui/react-label, @radix-ui/react-slot, utils, label, button

**Usage & accessibility:** A validation-aware form built on react-hook-form + zod — 8 composable parts. Wrap your <form> in <Form {...form}> (react-hook-form's FormProvider), then for each field use <FormField name=… control={form.control} render={({ field }) => …}>. Inside, FormItem groups FormLabel, FormControl (a Slot that wires id / aria-invalid / aria-describedby onto your input), FormDescription, and FormMessage (auto-renders the field's validation error). Define the schema with zod and pass zodResolver(schema) to useForm. The label turns red on error and everything is wired for accessibility automatically.

```tsx
// components/ui/form.tsx
"use client"

import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
  Controller,
  FormProvider,
  useFormContext,
  useFormState,
  type ControllerProps,
  type FieldPath,
  type FieldValues,
} from "react-hook-form"

import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"

const Form = FormProvider

type FormFieldContextValue<
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
> = {
  name: TName
}

const FormFieldContext = React.createContext<FormFieldContextValue>(
  {} as FormFieldContextValue
)

const FormField = <
  TFieldValues extends FieldValues = FieldValues,
  TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
>({
  ...props
}: ControllerProps<TFieldValues, TName>) => {
  return (
    <FormFieldContext.Provider value={{ name: props.name }}>
      <Controller {...props} />
    </FormFieldContext.Provider>
  )
}

const useFormField = () => {
  const fieldContext = React.useContext(FormFieldContext)
  const itemContext = React.useContext(FormItemContext)
  const { getFieldState } = useFormContext()
  const formState = useFormState({ name: fieldContext.name })
  const fieldState = getFieldState(fieldContext.name, formState)

  if (!fieldContext) {
    throw new Error("useFormField should be used within <FormField>")
  }

  const { id } = itemContext

  return {
    id,
    name: fieldContext.name,
    formItemId: `${id}-form-item`,
    formDescriptionId: `${id}-form-item-description`,
    formMessageId: `${id}-form-item-message`,
    ...fieldState,
  }
}

type FormItemContextValue = {
  id: string
}

const FormItemContext = React.createContext<FormItemContextValue>(
  {} as FormItemContextValue
)

function FormItem({ className, ...props }: React.ComponentProps<"div">) {
  const id = React.useId()

  return (
    <FormItemContext.Provider value={{ id }}>
      <div
        data-slot="form-item"
        className={cn("grid gap-2", className)}
        {...props}
      />
    </FormItemContext.Provider>
  )
}

function FormLabel({
  className,
  ...props
}: React.ComponentProps<typeof LabelPrimitive.Root>) {
  const { error, formItemId } = useFormField()

  return (
    <Label
      data-slot="form-label"
      data-error={!!error}
      className={cn("data-[error=true]:text-destructive", className)}
      htmlFor={formItemId}
      {...props}
    />
  )
}

function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
  const { error, formItemId, formDescriptionId, formMessageId } = useFormField()

  return (
    <Slot
      data-slot="form-control"
      id={formItemId}
      aria-describedby={
        !error
          ? `${formDescriptionId}`
          : `${formDescriptionId} ${formMessageId}`
      }
      aria-invalid={!!error}
      {...props}
    />
  )
}

function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
  const { formDescriptionId } = useFormField()

  return (
    <p
      data-slot="form-description"
      id={formDescriptionId}
      className={cn("text-muted-foreground text-sm", className)}
      {...props}
    />
  )
}

function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
  const { error, formMessageId } = useFormField()
  const body = error ? String(error?.message ?? "") : props.children

  if (!body) {
    return null
  }

  return (
    <p
      data-slot="form-message"
      id={formMessageId}
      className={cn("text-destructive text-sm", className)}
      {...props}
    >
      {body}
    </p>
  )
}

export {
  useFormField,
  Form,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  FormField,
}
```

## Examples

### Username (canonical)

The canonical react-hook-form + zod form — a single validated Input with label, description, and error message.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-demo-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, input, button, sonner

```tsx
// components/ui/form-demo-01.tsx
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
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"
import { Toaster } from "@/components/ui/sonner"

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

export default function FormDemo01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("You submitted the following values", {
      description: (
        <pre className="mt-2 w-full max-w-[320px] overflow-x-auto rounded-md bg-neutral-950 p-4">
          <code className="text-white">{JSON.stringify(values, null, 2)}</code>
        </pre>
      ),
    })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <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>
      <Toaster />
    </div>
  )
}
```

---

### Login form

Email + password with zod validation (valid email, min-length password) and a full-width submit.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-login-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, input, button, sonner

```tsx
// components/ui/form-login-01.tsx
"use client"

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

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

const formSchema = z.object({
  email: z.string().email({ message: "Enter a valid email address." }),
  password: z.string().min(8, {
    message: "Password must be at least 8 characters.",
  }),
})

export default function FormLogin01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { email: "", password: "" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Signed in", { description: values.email })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input type="email" placeholder="me@example.com" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Password</FormLabel>
                <FormControl>
                  <Input type="password" placeholder="••••••••" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit" className="w-full">
            Sign in
          </Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Sign-up form

Cross-field validation — a zod `.refine` confirms password and confirmation match, plus a required terms checkbox.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-signup-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, input, checkbox, button, sonner

```tsx
// components/ui/form-signup-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z
  .object({
    name: z.string().min(2, { message: "Name must be at least 2 characters." }),
    email: z.string().email({ message: "Enter a valid email address." }),
    password: z.string().min(8, {
      message: "Password must be at least 8 characters.",
    }),
    confirm: z.string(),
    terms: z.boolean().refine((v) => v === true, {
      message: "You must accept the terms.",
    }),
  })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords do not match.",
    path: ["confirm"],
  })

export default function FormSignup01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      name: "",
      email: "",
      password: "",
      confirm: "",
      terms: false,
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Account created", { description: values.email })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="name"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Name</FormLabel>
                <FormControl>
                  <Input placeholder="Ada Lovelace" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input type="email" placeholder="me@example.com" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="password"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Password</FormLabel>
                <FormControl>
                  <Input type="password" placeholder="••••••••" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="confirm"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Confirm password</FormLabel>
                <FormControl>
                  <Input type="password" placeholder="••••••••" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="terms"
            render={({ field }) => (
              <FormItem className="flex flex-row items-start gap-3 space-y-0">
                <FormControl>
                  <Checkbox
                    checked={field.value}
                    onCheckedChange={field.onChange}
                  />
                </FormControl>
                <div className="grid gap-1.5 leading-none">
                  <FormLabel className="font-normal">
                    Accept terms and conditions
                  </FormLabel>
                  <FormMessage />
                </div>
              </FormItem>
            )}
          />
          <Button type="submit" className="w-full">
            Create account
          </Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Contact form

Mixed field types — Input, a Select for the subject, and a Textarea message, each validated.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-contact-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, input, @designrevision/select, textarea, button, sonner

```tsx
// components/ui/form-contact-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Toaster } from "@/components/ui/sonner"
import { Textarea } from "@/components/ui/textarea"

const formSchema = z.object({
  name: z.string().min(2, { message: "Name must be at least 2 characters." }),
  email: z.string().email({ message: "Enter a valid email address." }),
  subject: z.string().min(1, { message: "Please choose a subject." }),
  message: z.string().min(10, {
    message: "Message must be at least 10 characters.",
  }),
})

export default function FormContact01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { name: "", email: "", subject: "", message: "" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Message sent", { description: `Subject: ${values.subject}` })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="name"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Name</FormLabel>
                <FormControl>
                  <Input placeholder="Ada Lovelace" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <FormControl>
                  <Input type="email" placeholder="me@example.com" {...field} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="subject"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Subject</FormLabel>
                <Select onValueChange={field.onChange} value={field.value}>
                  <FormControl>
                    <SelectTrigger className="w-full">
                      <SelectValue placeholder="Choose a topic" />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent>
                    <SelectItem value="sales">Sales</SelectItem>
                    <SelectItem value="support">Support</SelectItem>
                    <SelectItem value="billing">Billing</SelectItem>
                  </SelectContent>
                </Select>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="message"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Message</FormLabel>
                <FormControl>
                  <Textarea
                    placeholder="How can we help?"
                    className="resize-none"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit" className="w-full">
            Send message
          </Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Checkbox preferences

Boolean checkbox fields, with a required terms checkbox validated by a zod `.refine`.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-checkbox-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, checkbox, button, sonner

```tsx
// components/ui/form-checkbox-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z.object({
  marketing: z.boolean(),
  security: z.boolean(),
  terms: z.boolean().refine((v) => v === true, {
    message: "You must accept the terms to continue.",
  }),
})

export default function FormCheckbox01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { marketing: true, security: true, terms: false },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Preferences saved", {
      description: `Marketing: ${values.marketing ? "on" : "off"}`,
    })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="marketing"
            render={({ field }) => (
              <FormItem className="flex flex-row items-start gap-3 space-y-0">
                <FormControl>
                  <Checkbox
                    checked={field.value}
                    onCheckedChange={field.onChange}
                  />
                </FormControl>
                <div className="grid gap-1.5 leading-none">
                  <FormLabel className="font-normal">Marketing emails</FormLabel>
                  <FormDescription>
                    News about products and features.
                  </FormDescription>
                </div>
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="security"
            render={({ field }) => (
              <FormItem className="flex flex-row items-start gap-3 space-y-0">
                <FormControl>
                  <Checkbox
                    checked={field.value}
                    onCheckedChange={field.onChange}
                  />
                </FormControl>
                <div className="grid gap-1.5 leading-none">
                  <FormLabel className="font-normal">Security alerts</FormLabel>
                  <FormDescription>
                    Important notices about your account.
                  </FormDescription>
                </div>
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="terms"
            render={({ field }) => (
              <FormItem className="flex flex-row items-start gap-3 space-y-0">
                <FormControl>
                  <Checkbox
                    checked={field.value}
                    onCheckedChange={field.onChange}
                  />
                </FormControl>
                <div className="grid gap-1.5 leading-none">
                  <FormLabel className="font-normal">
                    Accept terms and conditions
                  </FormLabel>
                  <FormMessage />
                </div>
              </FormItem>
            )}
          />
          <Button type="submit">Save preferences</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Notification switches

Bordered rows pairing a label and description with a Switch, bound to boolean fields.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-switch-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, switch, button, sonner

```tsx
// components/ui/form-switch-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
} from "@/components/ui/form"
import { Switch } from "@/components/ui/switch"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z.object({
  marketing: z.boolean(),
  social: z.boolean(),
  security: z.boolean(),
})

const fields = [
  {
    name: "marketing" as const,
    label: "Marketing emails",
    description: "Receive emails about new products and features.",
  },
  {
    name: "social" as const,
    label: "Social notifications",
    description: "Receive emails for follows, likes, and comments.",
  },
  {
    name: "security" as const,
    label: "Security emails",
    description: "Receive emails about your account activity.",
  },
]

export default function FormSwitch01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { marketing: true, social: false, security: true },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Notifications updated", {
      description: `Security: ${values.security ? "on" : "off"}`,
    })
  }

  return (
    <div className="w-full max-w-md">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
          {fields.map((f) => (
            <FormField
              key={f.name}
              control={form.control}
              name={f.name}
              render={({ field }) => (
                <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
                  <div className="space-y-0.5">
                    <FormLabel>{f.label}</FormLabel>
                    <FormDescription>{f.description}</FormDescription>
                  </div>
                  <FormControl>
                    <Switch
                      checked={field.value}
                      onCheckedChange={field.onChange}
                    />
                  </FormControl>
                </FormItem>
              )}
            />
          ))}
          <Button type="submit">Save</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Radio group

A single-choice RadioGroup bound to a zod enum, with nested FormItems per option.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-radio-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, radio-group, button, sonner

```tsx
// components/ui/form-radio-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z.object({
  type: z.enum(["all", "mentions", "none"]),
})

const options = [
  { value: "all", label: "All new messages" },
  { value: "mentions", label: "Direct messages and mentions" },
  { value: "none", label: "Nothing" },
]

export default function FormRadio01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { type: "all" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Notification preference saved", { description: values.type })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="type"
            render={({ field }) => (
              <FormItem className="space-y-3">
                <FormLabel>Notify me about…</FormLabel>
                <FormControl>
                  <RadioGroup
                    onValueChange={field.onChange}
                    value={field.value}
                    className="flex flex-col space-y-1"
                  >
                    {options.map((option) => (
                      <FormItem
                        key={option.value}
                        className="flex items-center gap-3 space-y-0"
                      >
                        <FormControl>
                          <RadioGroupItem value={option.value} />
                        </FormControl>
                        <FormLabel className="font-normal">
                          {option.label}
                        </FormLabel>
                      </FormItem>
                    ))}
                  </RadioGroup>
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Save</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Date of birth

A Popover + Calendar bound to a zod date, with future and pre-1900 dates disabled.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-date-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, date-fns, form, calendar, popover, button, sonner

```tsx
// components/ui/form-date-01.tsx
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { format } from "date-fns"
import { CalendarIcon } from "lucide-react"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
import { z } from "zod"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Calendar } from "@/components/ui/calendar"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z.object({
  dob: z.date({ error: "A date of birth is required." }),
})

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

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Date of birth saved", {
      description: format(values.dob, "PPP"),
    })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <FormField
            control={form.control}
            name="dob"
            render={({ field }) => (
              <FormItem className="flex flex-col">
                <FormLabel>Date of birth</FormLabel>
                <Popover>
                  <PopoverTrigger asChild>
                    <FormControl>
                      <Button
                        variant="outline"
                        className={cn(
                          "w-full pl-3 text-left font-normal",
                          !field.value && "text-muted-foreground"
                        )}
                      >
                        {field.value ? (
                          format(field.value, "PPP")
                        ) : (
                          <span>Pick a date</span>
                        )}
                        <CalendarIcon className="ml-auto size-4 opacity-50" />
                      </Button>
                    </FormControl>
                  </PopoverTrigger>
                  <PopoverContent className="w-auto p-0" align="start">
                    <Calendar
                      mode="single"
                      selected={field.value}
                      onSelect={field.onChange}
                      disabled={(date) =>
                        date > new Date() || date < new Date("1900-01-01")
                      }
                      captionLayout="dropdown"
                    />
                  </PopoverContent>
                </Popover>
                <FormDescription>
                  Your date of birth is used to calculate your age.
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Submit</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Textarea with counter

A bio Textarea with a live character counter and zod min/max length validation.

**Install:**

```bash
npx shadcn@latest add @designrevision/form-textarea-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, textarea, button, sonner

```tsx
// components/ui/form-textarea-01.tsx
"use client"

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

import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Textarea } from "@/components/ui/textarea"
import { Toaster } from "@/components/ui/sonner"

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

export default function FormTextarea01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { bio: "" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Bio saved", { description: `${values.bio.length} characters` })
  }

  return (
    <div className="w-full max-w-sm">
      <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 a little bit about yourself"
                    className="resize-none"
                    {...field}
                  />
                </FormControl>
                <FormDescription>
                  {field.value.length}/160 characters
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Save</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```

---

### Account settings

A multi-field settings form — username (Input), verified email (Select), and bio (Textarea).

**Install:**

```bash
npx shadcn@latest add @designrevision/form-settings-01
```

**Dependencies:** react-hook-form, @hookform/resolvers, zod, form, input, @designrevision/select, textarea, button, sonner

```tsx
// components/ui/form-settings-01.tsx
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { toast } from "sonner"
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"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"
import { Toaster } from "@/components/ui/sonner"

const formSchema = z.object({
  username: z
    .string()
    .min(2, { message: "Username must be at least 2 characters." })
    .max(30, { message: "Username must be 30 characters or less." }),
  email: z.string().min(1, { message: "Please select a verified email." }),
  bio: z.string().max(160, { message: "Bio must be 160 characters or less." }),
})

export default function FormSettings01() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: { username: "", email: "", bio: "" },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    toast("Profile updated", { description: `@${values.username}` })
  }

  return (
    <div className="w-full max-w-sm">
      <Form {...form}>
        <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
          <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>
            )}
          />
          <FormField
            control={form.control}
            name="email"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Email</FormLabel>
                <Select onValueChange={field.onChange} value={field.value}>
                  <FormControl>
                    <SelectTrigger className="w-full">
                      <SelectValue placeholder="Select a verified email" />
                    </SelectTrigger>
                  </FormControl>
                  <SelectContent>
                    <SelectItem value="ada@example.com">
                      ada@example.com
                    </SelectItem>
                    <SelectItem value="lovelace@work.com">
                      lovelace@work.com
                    </SelectItem>
                  </SelectContent>
                </Select>
                <FormDescription>
                  Manage verified emails in your settings.
                </FormDescription>
                <FormMessage />
              </FormItem>
            )}
          />
          <FormField
            control={form.control}
            name="bio"
            render={({ field }) => (
              <FormItem>
                <FormLabel>Bio</FormLabel>
                <FormControl>
                  <Textarea
                    placeholder="Tell us about yourself"
                    className="resize-none"
                    {...field}
                  />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          <Button type="submit">Update profile</Button>
        </form>
      </Form>
      <Toaster />
    </div>
  )
}
```
