Form

Building forms with React Hook Form and Zod validation.

Installation

bash
npx shadcn@latest add formnpm install react-hook-form zod @hookform/resolvers

Import

import { useForm } from "react-hook-form"import { z } from "zod"import { zodResolver } from "@hookform/resolvers/zod"import {  Form,  FormControl,  FormDescription,  FormField,  FormItem,  FormLabel,  FormMessage,} from "@/components/ui/form"import { Input } from "@/components/ui/input"import { Button } from "@/components/ui/button"

Profile form

const formSchema = z.object({  username: z.string().min(2, { message: "Username must be at least 2 characters." }).max(50),})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)  }  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="Enter username..." {...field} />              </FormControl>              <FormDescription>This is your public display name.</FormDescription>              <FormMessage />            </FormItem>          )}        />        <Button type="submit">Submit</Button>      </form>    </Form>  )}

Multiple field types

const formSchema = z.object({  email: z.string().email(),  role: z.string({ required_error: "Please select a role." }),  notifications: z.boolean().default(false),})// Fields: Input for email, Select for role, Checkbox for notifications// Each follows the same FormField > FormItem > FormControl pattern

Props

PROPTYPEDEFAULTDESCRIPTION
FormFieldcontrol (from useForm), name (string), render (({ field }) => ReactNode)
FormItemWraps label + control + description + message (uses context)
FormControlWraps the actual input component
FormLabelRenders an accessible label linked to the control
FormDescriptionRenders helper text below the control
FormMessageAuto-displays the Zod validation error for this field

Key pattern

  • FormField render prop receives field — spread it onto your input: <Input {...field} />
  • FormMessage reads errors automatically from form state — no extra wiring needed