Form
Agrupa os campos e cuida da validação — nativa, customizada ou vinda do servidor — no envio.
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Form, FormActions } from '@/components/ui/form';
import { TextField } from '@/components/ui/input';
export default function FormDemo() {
const [enviado, setEnviado] = useState<string | null>(null);
return (
<Form
className="w-full max-w-sm"
onSubmit={(e) => {
e.preventDefault();
const dados = Object.fromEntries(new FormData(e.currentTarget));
setEnviado(String(dados.nome));
}}
onReset={() => setEnviado(null)}
>
<TextField name="nome" label="Nome" isRequired errorMessage="Informe seu nome." />
<TextField
name="email"
type="email"
label="E-mail"
isRequired
errorMessage={({ validationDetails }) =>
validationDetails.valueMissing ? 'Informe seu e-mail.' : 'Digite um e-mail válido.'
}
/>
<FormActions>
<Button type="reset" variant="outline">
Limpar
</Button>
<Button type="submit">Enviar</Button>
</FormActions>
{enviado && <p className="text-sm text-muted-foreground">Recebido! Obrigado, {enviado}.</p>}
</Form>
);
}Instalação
npx shadcn@latest add @lenstech/formUso
import { Form, FormActions } from "@/components/ui/form"<Form
onSubmit={(e) => {
e.preventDefault()
const dados = Object.fromEntries(new FormData(e.currentTarget))
enviar(dados)
}}
>
<TextField name="nome" label="Nome" isRequired />
<FormActions>
<Button type="submit">Enviar</Button>
</FormActions>
</Form>O Form é um <form> HTML real: não precisa de biblioteca de formulário. Todo campo com name entra no
FormData.
Três formas de validar
- Nativa — props nos campos:
isRequired,type="email",minLength,maxLength,pattern… Personalize a mensagem comerrorMessage. - Customizada —
validate={(valor) => "mensagem" | null}em cada campo. - Servidor —
validationErrors={{ email: "E-mail já cadastrado." }}noForm, indexado peloname.
Quando os erros aparecem
Com validationBehavior="native" (padrão), os erros só aparecem ao enviar: o envio é bloqueado e o foco vai
para o primeiro campo inválido. Com validationBehavior="aria", os erros aparecem enquanto o usuário digita e o
envio não é bloqueado — valide de novo no onSubmit.
Exemplos
Formulário de contato completo
Composição típica de site: TextField, Select,
Textarea, Checkbox de consentimento LGPD e Button
com isPending. Tente enviar vazio ou com um WhatsApp sem DDD.
'use client';
import { CheckCircle2, Loader2, Send } from 'lucide-react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import { Form, FormActions } from '@/components/ui/form';
import { TextField } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
const servicos = [
{ id: 'consulta', nome: 'Consulta de avaliação' },
{ id: 'limpeza', nome: 'Limpeza e prevenção' },
{ id: 'clareamento', nome: 'Clareamento' },
{ id: 'ortodontia', nome: 'Aparelho ortodôntico' },
{ id: 'implante', nome: 'Implante dentário' },
{ id: 'outro', nome: 'Outro' },
];
export default function FormContact() {
const [enviando, setEnviando] = useState(false);
const [enviado, setEnviado] = useState<string | null>(null);
if (enviado) {
return (
<div className="flex w-full max-w-xl flex-col items-start gap-3 rounded-xl border bg-card p-6 text-card-foreground">
<CheckCircle2 className="size-8 text-success" />
<h2 className="font-heading text-lg font-semibold">Mensagem enviada!</h2>
<p className="text-sm text-muted-foreground">
Obrigado, {enviado}. Nossa recepção responde em até 1 dia útil pelo e-mail ou WhatsApp informado.
</p>
<Button variant="outline" onPress={() => setEnviado(null)}>
Enviar outra mensagem
</Button>
</div>
);
}
return (
<Form
className="w-full max-w-xl rounded-xl border bg-card p-6 text-card-foreground shadow-xs"
onSubmit={(e) => {
e.preventDefault();
const dados = Object.fromEntries(new FormData(e.currentTarget));
setEnviando(true);
// Troque pelo envio real (fetch, server action…)
setTimeout(() => {
setEnviando(false);
setEnviado(String(dados.nome).split(' ')[0] ?? '');
}, 1500);
}}
>
<div className="flex flex-col gap-1.5">
<h2 className="font-heading text-xl font-semibold">Agende sua avaliação</h2>
<p className="text-sm text-muted-foreground">
Preencha os dados abaixo e a Clínica Sorriso Feliz entra em contato para confirmar o horário.
</p>
</div>
<TextField name="nome" label="Nome completo" autoComplete="name" isRequired errorMessage="Informe seu nome." />
<div className="grid gap-6 sm:grid-cols-2">
<TextField
name="email"
type="email"
label="E-mail"
autoComplete="email"
placeholder="voce@email.com.br"
isRequired
errorMessage={({ validationDetails }) =>
validationDetails.valueMissing ? 'Informe seu e-mail.' : 'Digite um e-mail válido.'
}
/>
<TextField
name="telefone"
type="tel"
label="WhatsApp"
autoComplete="tel"
placeholder="(41) 91234-5678"
isRequired
validate={(v) => (v && v.replace(/\D/g, '').length < 10 ? 'Inclua o DDD, ex.: (41) 91234-5678.' : null)}
errorMessage={({ validationDetails, validationErrors }) =>
validationDetails.valueMissing ? 'Informe seu WhatsApp.' : validationErrors.join(' ')
}
/>
</div>
<Select
name="servico"
label="Serviço de interesse"
placeholder="Selecione um serviço"
isRequired
errorMessage="Selecione um serviço."
>
<SelectTrigger />
<SelectContent items={servicos}>{(item) => <SelectItem>{item.nome}</SelectItem>}</SelectContent>
</Select>
<TextField
name="mensagem"
label="Mensagem"
multiline
rows={4}
placeholder="Ex.: Prefiro horários no fim da tarde, às terças ou quintas."
description="Opcional — conte o que precisa ou sua preferência de horário."
maxLength={500}
/>
<Checkbox
name="lgpd"
isRequired
description="Usaremos seus dados apenas para responder este contato, conforme a LGPD (Lei 13.709/2018)."
errorMessage="É preciso autorizar o contato para enviar."
>
Autorizo a clínica a entrar em contato comigo
</Checkbox>
<FormActions>
<Button type="submit" isPending={enviando} className="w-full sm:w-auto">
{enviando ? (
<>
<Loader2 className="animate-spin motion-reduce:animate-none" /> Enviando…
</>
) : (
<>
<Send /> Enviar mensagem
</>
)}
</Button>
</FormActions>
</Form>
);
}Validação customizada
validate pode devolver uma lista de mensagens — todas são exibidas.
'use client';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { TextField } from '@/components/ui/input';
export default function FormCustomValidation() {
return (
<Form onSubmit={(e) => e.preventDefault()} className="w-full max-w-sm">
<TextField
name="usuario"
label="Usuário do painel"
description="Letras minúsculas, números e ponto."
isRequired
validate={(v) => (/^[a-z0-9.]+$/.test(v) ? null : 'Use só letras minúsculas, números e ponto.')}
/>
<TextField
name="senha"
type="password"
label="Senha"
isRequired
validate={(v) => {
const erros: string[] = [];
if (v.length < 8) erros.push('Mínimo de 8 caracteres.');
if (!/\d/.test(v)) erros.push('Inclua pelo menos um número.');
return erros;
}}
/>
<Button type="submit" className="self-start">
Criar acesso
</Button>
</Form>
);
}Erros do servidor
O erro de validationErrors some quando o usuário edita o campo.
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Form } from '@/components/ui/form';
import { TextField } from '@/components/ui/input';
export default function FormServerErrors() {
const [erros, setErros] = useState<Record<string, string>>({});
const [enviando, setEnviando] = useState(false);
return (
<Form
validationErrors={erros}
className="w-full max-w-sm"
onSubmit={(e) => {
e.preventDefault();
setEnviando(true);
// Simula uma API que rejeita o e-mail
setTimeout(() => {
setEnviando(false);
setErros({ email: 'Este e-mail já está cadastrado. Tente entrar ou recuperar a senha.' });
}, 900);
}}
>
<TextField name="email" type="email" label="E-mail" defaultValue="contato@padariadourada.com.br" isRequired />
<Button type="submit" isPending={enviando} className="self-start">
{enviando ? 'Verificando…' : 'Cadastrar'}
</Button>
</Form>
);
}Validação em tempo real
'use client';
import { Form } from '@/components/ui/form';
import { TextField } from '@/components/ui/input';
export default function FormRealtime() {
return (
<Form validationBehavior="aria" onSubmit={(e) => e.preventDefault()} className="w-full max-w-sm">
<TextField
name="slug"
label="Endereço da loja"
description="Será usado em suaempresa.lenstech.com.br"
validate={(v) => (v && !/^[a-z0-9-]+$/.test(v) ? 'Use apenas letras minúsculas, números e hífen.' : null)}
/>
</Form>
);
}Quando usar
- Use sempre que houver campos enviados juntos: contato, orçamento, cadastro, login.
- Para filtros com efeito imediato (busca, ordenação), não é preciso
Form— use os campos soltos, comoSearchFieldeSelect. - Coloque os botões em
FormActionspara alinhá-los à direita no desktop e empilhá-los no celular.
API
Form
Aceita todas as props do Form do React Aria (e do <form> HTML). As
mais importantes:
Prop
Type
FormActions
<div> com as props HTML comuns. Linha de botões ao final do formulário.