Skip to main content

Installation

The Form component is built on top of React Hook Form and Zod for schema validation.
npm install react-hook-form @hookform/resolvers zod
Or use the CLI:
npx shadcn@latest add form

Usage

import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"

Create a form schema

Define your form schema using Zod:
import { z } from "zod"

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

Define a form

Use the useForm hook from React Hook Form to create a form:
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() {
  const form = useForm<z.infer<typeof formSchema>>({
    resolver: zodResolver(formSchema),
    defaultValues: {
      username: "",
    },
  })

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }
}

Build your form

Compose form fields using the Form components:
import { Button } from "@/components/ui/button"
import {
  Form,
  FormControl,
  FormDescription,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

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>
  )
}

Component API

Form

The root form provider built on React Hook Form’s FormProvider. Props:
  • All props from React Hook Form’s FormProvider

FormField

A controlled form field. Props:
  • control - Form control from useForm
  • name - Field name
  • render - Render function that receives field props

FormItem

Container for a form field.

FormLabel

Label for a form field. Props:
  • Automatically shows error state

FormControl

Wrapper for the form control element. Props:
  • Automatically handles id, aria-describedby, and aria-invalid

FormDescription

Description text for a form field.

FormMessage

Displays form field errors. Props:
  • Automatically displays the error message for the field

useFormField

A custom hook to access form field context. Returns:
  • id - Field ID
  • name - Field name
  • formItemId - Form item ID
  • formDescriptionId - Description ID
  • formMessageId - Message ID
  • error - Field error
  • invalid - Whether field is invalid
  • isDirty - Whether field is dirty
  • isTouched - Whether field is touched

Examples

Input

A simple form with an input field:
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).max(50),
})

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

  function onSubmit(values: z.infer<typeof formSchema>) {
    console.log(values)
  }

  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>
  )
}

Textarea

A form with a textarea:
import { Textarea } from "@/components/ui/textarea"

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

// In your form:
<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>
        You can <span>@mention</span> other users and organizations.
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Checkbox

A form with a checkbox:
import { Checkbox } from "@/components/ui/checkbox"

const formSchema = z.object({
  mobile: z.boolean().default(false).optional(),
})

// In your form:
<FormField
  control={form.control}
  name="mobile"
  render={({ field }) => (
    <FormItem className="flex flex-row items-start space-x-3 space-y-0">
      <FormControl>
        <Checkbox
          checked={field.value}
          onCheckedChange={field.onChange}
        />
      </FormControl>
      <div className="space-y-1 leading-none">
        <FormLabel>
          Use different settings for my mobile devices
        </FormLabel>
        <FormDescription>
          You can manage your mobile notifications in the mobile settings page.
        </FormDescription>
      </div>
    </FormItem>
  )}
/>

Select

A form with a select:
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"

const formSchema = z.object({
  email: z
    .string({
      required_error: "Please select an email to display.",
    })
    .email(),
})

// In your form:
<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Email</FormLabel>
      <Select onValueChange={field.onChange} defaultValue={field.value}>
        <FormControl>
          <SelectTrigger>
            <SelectValue placeholder="Select a verified email to display" />
          </SelectTrigger>
        </FormControl>
        <SelectContent>
          <SelectItem value="m@example.com">m@example.com</SelectItem>
          <SelectItem value="m@google.com">m@google.com</SelectItem>
          <SelectItem value="m@support.com">m@support.com</SelectItem>
        </SelectContent>
      </Select>
      <FormDescription>
        You can manage email addresses in your email settings.
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Radio Group

A form with a radio group:
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"

const formSchema = z.object({
  type: z.enum(["all", "mentions", "none"], {
    required_error: "You need to select a notification type.",
  }),
})

// In your form:
<FormField
  control={form.control}
  name="type"
  render={({ field }) => (
    <FormItem className="space-y-3">
      <FormLabel>Notify me about...</FormLabel>
      <FormControl>
        <RadioGroup
          onValueChange={field.onChange}
          defaultValue={field.value}
          className="flex flex-col space-y-1"
        >
          <FormItem className="flex items-center space-x-3 space-y-0">
            <FormControl>
              <RadioGroupItem value="all" />
            </FormControl>
            <FormLabel className="font-normal">
              All new messages
            </FormLabel>
          </FormItem>
          <FormItem className="flex items-center space-x-3 space-y-0">
            <FormControl>
              <RadioGroupItem value="mentions" />
            </FormControl>
            <FormLabel className="font-normal">
              Direct messages and mentions
            </FormLabel>
          </FormItem>
          <FormItem className="flex items-center space-x-3 space-y-0">
            <FormControl>
              <RadioGroupItem value="none" />
            </FormControl>
            <FormLabel className="font-normal">Nothing</FormLabel>
          </FormItem>
        </RadioGroup>
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

Switch

A form with a switch:
import { Switch } from "@/components/ui/switch"

const formSchema = z.object({
  marketing_emails: z.boolean().default(false).optional(),
})

// In your form:
<FormField
  control={form.control}
  name="marketing_emails"
  render={({ field }) => (
    <FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
      <div className="space-y-0.5">
        <FormLabel className="text-base">
          Marketing emails
        </FormLabel>
        <FormDescription>
          Receive emails about new products, features, and more.
        </FormDescription>
      </div>
      <FormControl>
        <Switch
          checked={field.value}
          onCheckedChange={field.onChange}
        />
      </FormControl>
    </FormItem>
  )}
/>

Accessibility

  • Built on React Hook Form for performance
  • Automatic error handling and display
  • Proper ARIA attributes
  • Label associations
  • Error announcements for screen readers

API Reference

See the React Hook Form and Zod documentation for complete API details.

Build docs developers (and LLMs) love