Form
Componente Form do shadcn - integração com react-hook-form com rótulos acessíveis, descrições e mensagens de erro.
Busque em todas as páginas da documentação
Componente Form do shadcn - integração com react-hook-form com rótulos acessíveis, descrições e mensagens de erro.
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥
Cartão de receita de referência rápida - pronto para copiar e colar.
npx shadcn@latest add form input button label"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Form, FormControl, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
const Schema = z.object({
email: z.string().email(),
name: z.string().min(2),
});
export function BasicForm() {
const form = useForm<z.infer<typeof Schema>>({
resolver: zodResolver(Schema),
defaultValues: { email: "", name: "" },
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(console.log)} className="space-y-4">
<FormField control={form.control} name="name" render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl><Input {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="email" render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl><Input type="email" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<Button type="submit">Submit</Button>
</form>
</Form>
);
}Quando usar isso: Quando você usa shadcn/ui e precisa de formulários com validação - o componente Form automatiza atributos ARIA, exibição de erros e associação de rótulos.
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import {
Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
import { toast } from "sonner";
const EventSchema = z.object({
title: z.string().min(1, "Title is required").max(100),
description: z.string().max(500).optional(),
date: z.string().min(1, "Date is required"),
time: z.string().min(1, "Time is required"),
location: z.string().min(1, "Location is required"),
type: z.enum(["conference", "meetup", "workshop", "webinar"]),
maxAttendees: z.coerce.number().int().positive().max(10000),
isPublic: z.boolean().default(true),
requiresRegistration: z.boolean().default(false),
});
type EventData = z.infer<typeof EventSchema>;
export function CreateEventForm() {
const form = useForm<EventData>({
resolver: zodResolver(EventSchema),
defaultValues: {
title: "",
description: "",
date: "",
time: "",
location: "",
type: "meetup",
maxAttendees: 100,
isPublic: true,
requiresRegistration: false,
},
});
async function onSubmit(data: EventData) {
await new Promise((r) => setTimeout(r, 1000));
toast.success("Event created!", { description: data.title });
form.reset();
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="max-w-lg space-y-6">
<FormField control={form.control} name="title" render={({ field }) => (
<FormItem>
<FormLabel>Event Title</FormLabel>
<FormControl><Input placeholder="React Conf 2025" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="description" render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl><Textarea placeholder="What is this event about?" rows={3} {...field} /></FormControl>
<FormDescription>Optional. Max 500 characters.</FormDescription>
<FormMessage />
</FormItem>
)} />
<div className="grid grid-cols-2 gap-4">
<FormField control={form.control} name="date" render={({ field }) => (
<FormItem>
<FormLabel>Date</FormLabel>
<FormControl><Input type="date" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="time" render={({ field }) => (
<FormItem>
<FormLabel>Time</FormLabel>
<FormControl><Input type="time" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<FormField control={form.control} name="location" render={({ field }) => (
<FormItem>
<FormLabel>Location</FormLabel>
<FormControl><Input placeholder="City, venue, or URL" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
<div className="grid grid-cols-2 gap-4">
<FormField control={form.control} name="type" render={({ field }) => (
<FormItem>
<FormLabel>Event Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger><SelectValue /></SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="conference">Conference</SelectItem>
<SelectItem value="meetup">Meetup</SelectItem>
<SelectItem value="workshop">Workshop</SelectItem>
<SelectItem value="webinar">Webinar</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} />
<FormField control={form.control} name="maxAttendees" render={({ field }) => (
<FormItem>
<FormLabel>Max Attendees</FormLabel>
<FormControl><Input type="number" {...field} /></FormControl>
<FormMessage />
</FormItem>
)} />
</div>
<FormField control={form.control} name="isPublic" render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div>
<FormLabel>Public Event</FormLabel>
<FormDescription>Anyone can discover this event.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)} />
<FormField control={form.control} name="requiresRegistration" render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div>
<FormLabel>Require Registration</FormLabel>
<FormDescription>Attendees must register in advance.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)} />
<Button type="submit" className="w-full" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? "Creating..." : "Create Event"}
</Button>
</form>
</Form>
);
}O que isso demonstra:
FormControl adequado para cada tipo de entrada<Form> é um provedor de contexto React que envolve FormProvider do react-hook-form<FormField> envolve o <Controller> do RHF - ele fornece field, fieldState e formState para a função de renderização<FormItem> gera um id único e o compartilha via contexto com os componentes filhos<FormLabel> renderiza um <label> com htmlFor automaticamente definido para o id do campo<FormControl> clona id, aria-invalid e aria-describedby em seu elemento filho de entrada<FormMessage> lê o erro de fieldState e o renderiza com o id correto para aria-describedby<FormDescription> renderiza texto de ajuda com um id também vinculado via aria-describedbyWrapper de campo reutilizável:
function TextField({
form,
name,
label,
placeholder,
description,
}: {
form: UseFormReturn<any>;
name: string;
label: string;
placeholder?: string;
description?: string;
}) {
return (
<FormField
control={form.control}
name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input placeholder={placeholder} {...field} />
</FormControl>
{description && <FormDescription>{description}</FormDescription>}
<FormMessage />
</FormItem>
)}
/>
);
}Formulário em um diálogo:
<Dialog>
<DialogTrigger asChild><Button>Edit</Button></DialogTrigger>
<DialogContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader>
<DialogTitle>Edit Item</DialogTitle>
</DialogHeader>
{/* FormFields aqui */}
<DialogFooter>
<Button type="submit">Save</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>// FormField é genérico - detecta nomes de campo incorretos
<FormField
control={form.control}
name="nonexistent" // Erro TS se não estiver no schema
render={({ field }) => <Input {...field} />}
/>
// field.value é tipado por campo
<FormField name="type" render={({ field }) => {
field.value; // "conference" | "meetup" | "workshop" | "webinar"
}} />
// Tipo UseFormReturn para passar o form para filhos
import type { UseFormReturn } from "react-hook-form";
function Subform({ form }: { form: UseFormReturn<EventData> }) { ... }Select precisa de onValueChange, não {...field} - Radix Select não usa eventos nativos. Correção: Use onValueChange={field.onChange} e defaultValue={field.value}.
Switch precisa de checked + onCheckedChange - Não espalhe {...field}. Correção: Use checked={field.value} e onCheckedChange={field.onChange}.
FormControl deve envolver um elemento - Ele clona props ARIA em seu único filho. Múltiplos filhos o quebram. Correção: Envolva apenas o elemento de entrada em FormControl.
Entradas numéricas retornam strings - <Input type="number" {...field}> retorna uma string. Correção: Use z.coerce.number() no schema para lidar com a coerção.
FormMessage não renderiza nada quando válido - FormMessage só renderiza quando há um erro. Nenhuma condição extra é necessária.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| RHF Puro + register | Você usa entradas nativas sem uma biblioteca de componentes | Você usa shadcn e quer formulários acessíveis consistentes |
| Formik + Yup | Você está em uma base de código baseada em Formik | Começando do zero com React moderno |
| Formulário Nativo + Server Actions | Você quer JavaScript mínimo no cliente | Você precisa de validação instantânea em nível de campo |
| AutoForm (extensão shadcn) | Você quer formulários gerados automaticamente a partir de schemas Zod | Você precisa de layout e comportamento personalizados |
react-hook-form para gerenciamento de estado e validação de formulárioszod para validação baseada em schema@hookform/resolvers/zod para conectar schemas Zod ao react-hook-formForm - envolve FormProvider do RHF, compartilha o contexto do formulárioFormField - envolve Controller do RHF, fornece field e fieldStateFormItem - gera um id único compartilhado por rótulo, entrada e mensagemFormLabel - renderiza <label> com htmlFor definido automaticamenteFormControl - clona id, aria-invalid, aria-describedby no elemento filho de entradaFormMessage - renderiza mensagem de erro vinculada via aria-describedby<FormField name="type" render={({ field }) => (
<FormItem>
<FormLabel>Type</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger><SelectValue /></SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="meetup">Meetup</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)} />onValueChange em vez de onChange, e defaultValue em vez de valuechecked e onCheckedChange em vez de props de entrada padrão{...field}<Input type="number" {...field}> sempre retorna um valor de stringz.coerce.number() no schema Zod para converter strings em números durante a validaçãofunction TextField({ form, name, label, placeholder }: {
form: UseFormReturn<any>;
name: string;
label: string;
placeholder?: string;
}) {
return (
<FormField control={form.control} name={name}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl><Input placeholder={placeholder} {...field} /></FormControl>
<FormMessage />
</FormItem>
)}
/>
);
}FormField é genérico e tipado contra o schema Zodname="nonexistent" produz um erro TypeScript se o campo não estiver no schemafield.value também é tipado por campo (por exemplo, um tipo union para enums)FormControl clona atributos ARIA (id, aria-invalid, aria-describedby) em seu único filho<DialogContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<DialogHeader><DialogTitle>Edit</DialogTitle></DialogHeader>
{/* FormFields aqui */}
<DialogFooter><Button type="submit">Save</Button></DialogFooter>
</form>
</Form>
</DialogContent><FormField name="isPublic" render={({ field }) => (
<FormItem className="flex items-center justify-between rounded-lg border p-3">
<div>
<FormLabel>Public</FormLabel>
<FormDescription>Visible to everyone.</FormDescription>
</div>
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
</FormItem>
)} />import type { UseFormReturn } from "react-hook-form";
function Subform({ form }: { form: UseFormReturn<EventData> }) { ... }FormMessage só renderiza quando fieldState contém um erroRevisado por Chris St. John·Última atualização: 19 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥