React

shadcn forms with react-hook-form

Declare the Zod schema, mount Form with useForm, and let FormField wire input, error and ARIA attributes. The shadcn classes stay as they are.

Key points

  • The Zod schema is the only source of truth: the form reads its errors, it does not restate them.
  • FormField wires input, message and ARIA attributes, so do not replace FormControl.
  • react-hook-form 7.91.0 and zod 4.6.7 are both MIT and must be installed separately.

Short answer: install react-hook-form, zod and @hookform/resolvers, declare the schema, and mount Form with useForm. Each FormField wires the input, the error message and the ARIA attributes.

The schema before the form

import { z } from "zod";

export const signupSchema = z.object({
  email: z.email(),
  password: z.string().min(12, "Almeno 12 caratteri"),
});

export type SignupValues = z.infer;

The schema does two jobs: it validates values at runtime and produces the SignupValues type at compile time. The resolver turns the Zod result into the shape react-hook-form expects, so errors arrive already tied to their field.

The complete form

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
  Form,
  FormControl,
  FormField,
  FormItem,
  FormLabel,
  FormMessage,
} from "@/components/ui/form";
import { signupSchema, type SignupValues } from "./schema";

export function SignupForm() {
  const form = useForm({
    resolver: zodResolver(signupSchema),
    defaultValues: { email: "", password: "" },
    mode: "onBlur",
  });

  return (
    
       console.log(values))} noValidate>
         (
            
              Email
              
                
              
              
            
          )}
        />
        Crea account
      
    
  );
}

Three details hold it together: Form publishes the form context, FormControl adds aria-invalid and aria-describedby when there is an error, and FormMessage prints the message only while that field is invalid.

What FormField does for you

  • One render per keystroke: the field updates itself, not the whole form, because Controller subscribes to a single name.
  • Accessible wiring: label, description and error reach the input through generated ids, so a screen reader reads them.
  • Typed state: control knows the SignupValues type, and a wrong field name becomes a compile error.

The command

npx shadcn@latest add form input button
npm install react-hook-form zod @hookform/resolvers

Live records in the catalogue

Compare the result with records already published: the shadcn library, the form-layout-01 block with labels and columns, and the form blocks category. Every record names its licence and source.

The limitation

The schema validates in the browser, not on the server: run the same rule there, or a hand-built request skips it. The form also mounts with noValidate, so native browser validation stays off and the only messages are the ones you write.

Where to continue

Component installation is in how to install shadcn/ui; the same validation on the server is in Server Actions and validation; the comparison between form libraries is in React form libraries. For the styling tokens read how to theme shadcn/ui.

Frequently asked questions

Do I install react-hook-form and zod by hand?

Yes. The shadcn command adds only the interface components; react-hook-form, zod and the resolver are project dependencies and go in with npm.

Can I use Form without Zod?

Yes. Form accepts any resolver that works with react-hook-form, Yup or Valibot for instance, or inline rules. Zod adds the type inferred from the schema, which inline rules do not give you.

Why does FormMessage stay empty?

Because the field has not been validated yet, usually from a lazy mode or from a name that does not match. Check the mode passed to useForm and the name on FormField.

How do I show an error coming from the server?

Call form.setError with the field name after the response. The message goes through the same channel as FormMessage, so remote errors need no second component.

Does the form work without JavaScript?

No. react-hook-form validates and submits in the browser, so without JavaScript the submit never fires. A form that must work without JavaScript needs plain HTML with a server action.