Formulários com Zod
Passe um schema Zod pro Form e conecte cada campo pela prop name. Valide ao sair do campo e receba dados tipados no envio.
Exemplo
Teste o fluxo: envie o formulário vazio, saia de um campo inválido e corrija o valor.
form-zod.tsx
"use client";
import { useState } from "react";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
const schema = z.object({
name: z.string().trim().min(2, "enter at least 2 letters."),
email: z.email("that email doesn't look valid."),
password: z.string().min(8, "the password needs 8 characters or more."),
terms: z.literal(true, "accept the terms to continue."),
});
export default function FormZod() {
const [created, setCreated] = useState<string | null>(null);
return (
<Form
className="w-full max-w-sm"
onSubmit={async (values) => {
// `values` is already validated and typed: { name: string; email: string; password: string; terms: true }
await new Promise((r) => setTimeout(r, 800));
setCreated(values.name);
}}
schema={schema}
>
<Field name="name">
<FieldLabel>name</FieldLabel>
<Input autoComplete="name" />
<FieldError />
</Field>
<Field name="email">
<FieldLabel>email</FieldLabel>
<Input autoComplete="email" inputMode="email" />
<FieldError />
</Field>
<Field name="password">
<FieldLabel>password</FieldLabel>
<Input autoComplete="new-password" type="password" />
<FieldError />
</Field>
<Field name="terms">
<FieldLabel className="flex items-center gap-2 font-normal">
<Checkbox /> I accept the terms of use
</FieldLabel>
<FieldError />
</Field>
<Button type="submit" variant="brand">
create account
</Button>
{created && (
<p aria-live="polite" className="text-sm">
account created, {created}!
</p>
)}
</Form>
);
}Como funciona
- O
Formrecebe o schema e prepara a validação dos campos. - Cada
Fieldcomnamevalida a própria chave do schema quando você sai do campo. Mude esse momento comvalidationMode. - No envio, o schema inteiro é validado. Se houver erro, ele aparece no
FieldErrorcorrespondente e o foco vai pro primeiro campo inválido. Se estiver tudo certo,onSubmitrecebe os dados convertidos e tipados pelo schema.
const schema = z.object({
email: z.email("invalid email"),
idade: z.coerce.number().min(18, "precisa ter 18+"),
})
<Form schema={schema} onSubmit={(values) => {
values.idade // number, não string
}}>
<Field name="email">…</Field>
<Field name="idade">…</Field>
</Form>Dica: com schema, prefira inputMode="email" em vez de type="email". O teclado do celular continua igual; a mensagem de erro vem do schema, não do navegador.
Ainda mais leve com zod/mini
O Form importa só o núcleo do Zod (zod/v4/core), então também aceita schemas de zod/mini. Com ele, o bundle fica mais enxuto.
import * as z from "zod/mini"
const schema = z.object({
email: z.email("invalid email"),
password: z.string().check(z.minLength(8, "minimum 8 characters")),
})Erros do servidor
Quando o back-end encontrar um erro, como um e-mail já cadastrado, passe a mensagem pela prop errors:
const [errors, setErrors] = useState({})
<Form schema={schema} errors={errors} onSubmit={async (values) => {
const res = await signup(values)
if (!res.ok) setErrors({ email: "that email already has an account." })
}}>Instalar
$ npx shadcn@latest add https://cd-ui.vercel.app/r/form.json https://cd-ui.vercel.app/r/input.json