Join our Discord Community

Form

Build accessible, type-safe forms with React Hook Form and Zod validation. Composable components that handle state, validation, and accessibility automatically.

Building forms in React used to suck. This setup combines React Hook Form with Zod validation so you get type safety, error handling, and all that good stuff without the usual headaches.

Simple form with validation

A clean, accessible form with real-time validation:

Loading component...

React Hook Form handles the state, Zod catches the errors, and the accessibility stuff happens automatically.

npx shadcn@latest add form

Why this doesn't suck like other form libraries

You've probably tried building forms in React before and wanted to give up. This actually works:

  • Type safety - Zod catches your mistakes before users see them
  • Fast - React Hook Form doesn't re-render everything on every keystroke
  • Actually accessible - Screen readers work, keyboard nav works
  • Composable - Mix and match components however you need
  • Real-time validation - Show errors as people type, not after they submit
  • Server-side too - Validate on both ends without duplicate code
  • Just React - Not tied to Next.js or any other framework

Essential form patterns you'll build

Profile form

Username, email, bio - the full profile setup:

Loading component...

Select dropdown

Dropdowns that actually validate:

Loading component...

Checkbox groups

Pick multiple options, validate the whole array:

Loading component...

Search and select with validation:

Loading component...

Date picker

Date picker that actually validates dates:

Loading component...

How to actually use this

1. Write your schema

Tell Zod what you expect:

import { z } from "zod"

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

2. Hook it up

Connect React Hook Form:

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

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

3. Build the thing

Put it all together:

<Form {...form}>
  <form onSubmit={form.handleSubmit(onSubmit)}>
    <FormField
      control={form.control}
      name="username"
      render={({ field }) => (
        <FormItem>
          <FormLabel>Username</FormLabel>
          <FormControl>
            <Input {...field} />
          </FormControl>
          <FormMessage />
        </FormItem>
      )}
    />
  </form>
</Form>

API Reference

Form

Root form provider that connects React Hook Form with form components.

PropTypeDescription
...formPropsUseFormReturn<TFieldValues>React Hook Form return object
childrenReact.ReactNodeForm content and fields

FormField

Controlled form field that handles validation and state.

PropTypeDescription
controlControl<TFieldValues>Form control from useForm
namePath<TFieldValues>Field name matching schema
render({ field, fieldState, formState }) => React.ReactNodeRender function for field UI
shouldUnregisterbooleanWhether to unregister field on unmount
defaultValueTFieldValueDefault value for uncontrolled usage
rulesRegisterOptionsAdditional validation rules

FormItem

Container for form field with proper spacing and context.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-FormLabel, FormControl, FormMessage

FormLabel

Accessible label that connects to form field automatically.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Label text

FormControl

Wrapper that provides field connection and accessibility attributes.

PropTypeDefaultDescription
childrenReact.ReactElement-Single form input component

FormDescription

Optional helper text that provides additional context.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Description text

FormMessage

Error message that displays validation feedback automatically.

PropTypeDefaultDescription
classNamestring-Additional CSS classes
childrenReact.ReactNode-Custom error message (optional)

Validation Modes

ModeDescriptionUse Case
onSubmitValidate on form submissionDefault, best performance
onBlurValidate when field loses focusBetter UX for long forms
onChangeValidate on every changeReal-time feedback needed
onTouchedValidate after first interactionBalance of UX and performance
allValidate on all eventsMaximum feedback

Field State Properties

PropertyTypeDescription
field.valueanyCurrent field value
field.onChange(value: any) => voidChange handler
field.onBlur() => voidBlur handler
field.namestringField name
field.refReact.RefField reference
fieldState.invalidbooleanWhether field has errors
fieldState.isTouchedbooleanWhether field was interacted with
fieldState.isDirtybooleanWhether field value changed
fieldState.errorFieldErrorValidation error object

Common Validation Patterns

String validation

z.string()
  .min(2, "Too short")
  .max(50, "Too long")
  .email("Invalid email")
  .url("Invalid URL")
  .regex(/^[A-Z0-9]+$/, "Uppercase letters and numbers only")

Number validation

z.number()
  .min(0, "Must be positive")
  .max(100, "Must be under 100")
  .int("Must be whole number")

Array validation

z.array(z.string())
  .min(1, "Select at least one")
  .max(5, "Select up to 5 items")

Object validation

z.object({
  name: z.string().min(1),
  email: z.string().email(),
}).refine((data) => data.name !== data.email, {
  message: "Name and email cannot be the same",
  path: ["name"],
})

Form best practices

Create forms users actually want to fill out:

  • Clear labels - Use descriptive, action-oriented labels that explain what's needed
  • Helpful placeholders - Show format examples, not just repeat the label
  • Smart defaults - Pre-fill fields when you have the information
  • Real-time validation - Catch errors early, but don't be overly aggressive
  • Progress indicators - Show users how much more they need to complete
  • Error recovery - Make it easy to understand and fix validation errors
  • Logical grouping - Organize related fields together with clear sections
  • Mobile optimization - Use appropriate input types and keyboard layouts
  • Save progress - Allow users to continue later for longer forms