shadcn Form
Crie formulários com o componente Form do shadcn - react-hook-form + Zod + componentes de UI acessíveis, todos interligados.
Busque em todas as páginas da documentação
Crie formulários com o componente Form do shadcn - react-hook-form + Zod + componentes de UI acessíveis, todos interligados.
🤖 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.
"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, FormDescription, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
const Schema = z.object({
username: z.string().min(2, "Mínimo de 2 caracteres"),
email: z.string().email("Email inválido"),
});
type FormData = z.infer<typeof Schema>;
export function MyForm() {
const form = useForm<FormData>({
resolver: zodResolver(Schema),
defaultValues: { username: "", email: "" },
});
return (
<Form {...form}>
<form onSubmit={form.handleSubmit((data) => console.log(data))} className="space-y-4">
<FormField
control={form.control}
name="username"
render={({ field }) => (
<FormItem>
<FormLabel>Nome de usuário</FormLabel>
<FormControl>
<Input placeholder="johndoe" {...field} />
</FormControl>
<FormDescription>Seu nome de exibição público.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="john@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit">Enviar</Button>
</form>
</Form>
);
}Quando usar isso: Quando você usa shadcn/ui e deseja campos de formulário consistentes e acessíveis com exibição de erros, rótulos e descrições integrados.
"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 { Checkbox } from "@/components/ui/checkbox";
import {
Form, FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage,
} from "@/components/ui/form";
const FeedbackSchema = z.object({
name: z.string().min(1, "O nome é obrigatório"),
email: z.string().email("Email inválido"),
category: z.enum(["bug", "feature", "question", "other"], {
required_error: "Selecione uma categoria",
}),
message: z.string().min(10, "Mínimo de 10 caracteres").max(1000),
priority: z.enum(["low", "medium", "high"]).default("medium"),
subscribe: z.boolean().default(false),
});
type FeedbackData = z.infer<typeof FeedbackSchema>;
export function FeedbackForm() {
const form = useForm<FeedbackData>({
resolver: zodResolver(FeedbackSchema),
defaultValues: {
name: "",
email: "",
message: "",
priority: "medium",
subscribe: false,
},
});
async function onSubmit(data: FeedbackData) {
await new Promise((r) => setTimeout(r, 1000));
console.log("Feedback:", data);
form.reset();
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="max-w-lg space-y-6">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Nome</FormLabel>
<FormControl>
<Input placeholder="Seu nome" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Categoria</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Selecione a categoria" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="bug">Relatório de Bug</SelectItem>
<SelectItem value="feature">Solicitação de Recurso</SelectItem>
<SelectItem value="question">Pergunta</SelectItem>
<SelectItem value="other">Outro</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
render={({ field }) => (
<FormItem>
<FormLabel>Prioridade</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="low">Baixa</SelectItem>
<SelectItem value="medium">Média</SelectItem>
<SelectItem value="high">Alta</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="message"
render={({ field }) => (
<FormItem>
<FormLabel>Mensagem</FormLabel>
<FormControl>
<Textarea placeholder="Descreva seu feedback..." rows={4} {...field} />
</FormControl>
<FormDescription>Mínimo de 10 caracteres.</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="subscribe"
render={({ field }) => (
<FormItem className="flex 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>Atualizações por email</FormLabel>
<FormDescription>Receba notificações sobre o status do seu feedback.</FormDescription>
</div>
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting} className="w-full">
{form.formState.isSubmitting ? "Enviando..." : "Enviar Feedback"}
</Button>
</form>
</Form>
);
}O que isso demonstra:
onValueChange / defaultValue<Form> é um provedor de contexto que passa o valor de retorno de useForm para todos os filhos.<FormField> é um wrapper em torno do <Controller> do RHF - ele fornece field, fieldState e formState.<FormItem> estabelece um contexto com um id gerado para associação rótulo-entrada.<FormLabel> renderiza um <label> com o htmlFor correto e estilo de erro.<FormControl> passa aria-invalid, aria-describedby e o id para a entrada.<FormMessage> lê o erro do contexto do campo e o renderiza com role="alert".<FormDescription> renderiza texto de ajuda vinculado via aria-describedby.Grupo de Rádio:
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
<FormField
control={form.control}
name="plan"
render={({ field }) => (
<FormItem>
<FormLabel>Plano</FormLabel>
<FormControl>
<RadioGroup onValueChange={field.onChange} defaultValue={field.value} className="flex gap-4">
<FormItem className="flex items-center space-x-2 space-y-0">
<FormControl><RadioGroupItem value="free" /></FormControl>
<FormLabel className="font-normal">Grátis</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-2 space-y-0">
<FormControl><RadioGroupItem value="pro" /></FormControl>
<FormLabel className="font-normal">Pro</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>Seletor de Data:
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { Calendar } from "@/components/ui/calendar";
<FormField
control={form.control}
name="date"
render={({ field }) => (
<FormItem>
<FormLabel>Data</FormLabel>
<Popover>
<PopoverTrigger asChild>
<FormControl>
<Button variant="outline">
{field.value ? format(field.value, "PPP") : "Escolha uma data"}
</Button>
</FormControl>
</PopoverTrigger>
<PopoverContent>
<Calendar mode="single" selected={field.value} onSelect={field.onChange} />
</PopoverContent>
</Popover>
<FormMessage />
</FormItem>
)}
/>// FormField é genérico - name é verificado quanto ao tipo em relação ao schema do formulário
<FormField
control={form.control}
name="typo" // Erro TS: "typo" não está em FeedbackData
render={({ field }) => /* ... */}
/>
// field.value é tipado por campo
<FormField
name="category"
render={({ field }) => {
field.value; // "bug" | "feature" | "question" | "other"
}}
/>Select precisa de onValueChange, não onChange - Radix Select não usa eventos de mudança nativos. Correção: Conecte field.onChange a onValueChange e field.value a defaultValue.
Checkbox retorna boolean, não string - Use field.value (booleano) com checked e field.onChange com onCheckedChange. Não use o spread {...field} diretamente.
FormControl deve envolver exatamente uma entrada - Ele clona as props aria-* e id em seu único filho. Envolver múltiplos elementos quebra a associação. Correção: Envolva apenas o elemento de entrada.
Valores padrão ausentes - Se defaultValues estiver incompleto, o Select do shadcn exibirá um gatilho em branco em vez do placeholder. Correção: Forneça defaultValues completos ou use defaultValue no Select.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| RHF + Zod Puros | Você usa entradas HTML nativas sem uma biblioteca de componentes | Você usa componentes shadcn/ui |
| Formik + MUI | Você está em uma base de código Material UI | Você usa Tailwind / shadcn |
| Mantine form | Você usa a biblioteca de componentes Mantine | Você usa shadcn |
| Primitivas de formulário personalizadas | Você precisa de um sistema de design totalmente personalizado | shadcn atende às suas necessidades |
<Form> é um provedor de contexto que passa o valor de retorno de useForm para todos os filhos. Componentes filhos como <FormField>, <FormLabel>, <FormControl> e <FormMessage> leem desse contexto para conectar IDs, atributos aria e exibição de erros.
<FormField> é um wrapper em torno do <Controller> do RHF. Sua prop render fornece field, fieldState e formState, assim como o Controller faz.
id gerado na entrada para associação de rótuloaria-invalid quando o campo tem um erroaria-describedby vinculando aos elementos de descrição e mensagem de erro<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Escolha um" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="a">Opção A</SelectItem>
</SelectContent>
</Select>Use onValueChange, não onChange, pois o Radix Select não usa eventos de mudança nativos.
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>Não use o spread {...field} diretamente no Checkbox, pois ele espera checked (booleano), não value.
Ele renderiza texto de ajuda abaixo da entrada e é automaticamente vinculado via aria-describedby para que leitores de tela o anunciem quando a entrada estiver focada.
<FormMessage> lê o erro do contexto do campo (fornecido por FormField) e renderiza a mensagem de erro com role="alert". Nenhuma verificação manual de erro é necessária.
Isso acontece quando defaultValues em useForm está incompleto ou ausente para esse campo. Forneça defaultValues completos para todos os campos ou defina defaultValue diretamente no componente Select.
FormControl clona props aria-* e id em seu único filho. Envolver múltiplos elementos quebra a associação rótulo-entrada e os atributos de acessibilidade. Sempre envolva exatamente um elemento de entrada.
<RadioGroup
onValueChange={field.onChange}
defaultValue={field.value}
>
<FormItem className="flex items-center space-x-2">
<FormControl><RadioGroupItem value="free" /></FormControl>
<FormLabel>Grátis</FormLabel>
</FormItem>
</RadioGroup>Sim. FormField é genérico e verifica name em relação ao tipo do schema do formulário. Passar um nome que não existe no schema produz um erro TypeScript.
Sim. Para um campo enum como category: z.enum(["bug", "feature"]), field.value é tipado como "bug" | "feature", não string.
Sim. Use um Popover com um Calendar dentro de FormField. Conecte field.value a selected e field.onChange a onSelect no componente Calendar.
Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥