Exibição de Erros em Formulários
Erros de campo, erros de formulário, notificações toast e mensagens inline - padrões para exibir feedback de validação aos usuários.
Busque em todas as páginas da documentação
Erros de campo, erros de formulário, notificações toast e mensagens inline - padrões para exibir feedback de validação aos usuários.
🤖 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";
const Schema = z.object({
email: z.string().email("Email inválido"),
password: z.string().min(8, "Pelo menos 8 caracteres"),
});
function FormWithErrors() {
const { register, handleSubmit, formState: { errors }, setError } = useForm({
resolver: zodResolver(Schema),
});
return (
<form onSubmit={handleSubmit(async (data) => {
const res = await fetch("/api/login", { method: "POST", body: JSON.stringify(data) });
if (!res.ok) setError("root", { message: "Credenciais inválidas" });
})}>
{/* Erro em nível de formulário */}
{errors.root && (
<div role="alert" className="rounded bg-red-50 p-3 text-sm text-red-700">
{errors.root.message}
</div>
)}
{/* Erro em nível de campo */}
<div>
<input {...register("email")} aria-invalid={!!errors.email} aria-describedby="email-error" />
{errors.email && <p id="email-error" role="alert" className="text-sm text-red-600">{errors.email.message}</p>}
</div>
<div>
<input {...register("password")} type="password" aria-invalid={!!errors.password} />
{errors.password && <p role="alert" className="text-sm text-red-600">{errors.password.message}</p>}
</div>
<button type="submit">Enviar</button>
</form>
);
}Quando usar isso: Todo formulário precisa de exibição de erros. Escolha o padrão com base no tipo de erro - nível de campo para validação, nível de formulário para erros do servidor, toast para resultados assíncronos.
"use client";
import { useState, useEffect, useRef } from "react";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
const Schema = z.object({
name: z.string().min(1, "O nome é obrigatório"),
email: z.string().email("Por favor, insira um endereço de email válido"),
phone: z.string().regex(/^\+?[\d\s-()]+$/, "Número de telefone inválido").optional().or(z.literal("")),
message: z.string().min(20, "A mensagem deve ter pelo menos 20 caracteres"),
});
type FormData = z.infer<typeof Schema>;
// Componente de erro inline com animação
function FieldError({ message }: { message?: string }) {
if (!message) return null;
return (
<p role="alert" className="mt-1 animate-[slideDown_0.2s_ease-out] text-sm text-red-600">
{message}
</p>
);
}
// Banner de erro em nível de formulário
function FormBanner({ message, type }: { message: string; type: "error" | "success" }) {
const colors = {
error: "bg-red-50 border-red-200 text-red-800",
success: "bg-green-50 border-green-200 text-green-800",
};
return (
<div role="alert" className={`rounded border p-3 text-sm ${colors[type]}`}>
{message}
</div>
);
}
// Notificação toast
function Toast({ message, onClose }: { message: string; onClose: () => void }) {
useEffect(() => {
const timer = setTimeout(onClose, 5000);
return () => clearTimeout(timer);
}, [onClose]);
return (
<div
role="status"
aria-live="polite"
className="fixed bottom-4 right-4 rounded-lg bg-gray-900 px-4 py-3 text-sm text-white shadow-lg"
>
{message}
<button onClick={onClose} className="ml-3 text-gray-400 hover:text-white">
Fechar
</button>
</div>
);
}
export function ContactFormWithErrors() {
const [toast, setToast] = useState<string | null>(null);
const errorSummaryRef = useRef<HTMLDivElement>(null);
const {
register,
handleSubmit,
formState: { errors, isSubmitting, submitCount },
setError,
reset,
} = useForm<FormData>({
resolver: zodResolver(Schema),
mode: "onBlur",
});
// Foca no resumo de erros quando eles aparecem
useEffect(() => {
if (Object.keys(errors).length > 0 && submitCount > 0) {
errorSummaryRef.current?.focus();
}
}, [errors, submitCount]);
async function onSubmit(data: FormData) {
try {
const res = await fetch("/api/contact", {
method: "POST",
body: JSON.stringify(data),
headers: { "Content-Type": "application/json" },
});
if (!res.ok) throw new Error("Erro no servidor");
reset();
setToast("Mensagem enviada com sucesso!");
} catch {
setError("root", { message: "Falha ao enviar. Por favor, tente novamente." });
}
}
const errorCount = Object.keys(errors).filter((k) => k !== "root").length;
return (
<>
<form onSubmit={handleSubmit(onSubmit)} className="max-w-md space-y-4" noValidate>
{/* Resumo de erros no topo */}
{errorCount > 0 && submitCount > 0 && (
<div
ref={errorSummaryRef}
tabIndex={-1}
role="alert"
className="rounded border border-red-200 bg-red-50 p-3 outline-none"
>
<p className="font-medium text-red-800">
Por favor, corrija {errorCount} {errorCount === 1 ? "erro" : "erros"}:
</p>
<ul className="mt-1 list-inside list-disc text-sm text-red-700">
{errors.name && <li>{errors.name.message}</li>}
{errors.email && <li>{errors.email.message}</li>}
{errors.phone && <li>{errors.phone.message}</li>}
{errors.message && <li>{errors.message.message}</li>}
</ul>
</div>
)}
{/* Erro do servidor */}
{errors.root && <FormBanner message={errors.root.message!} type="error" />}
<div>
<label htmlFor="name" className="block text-sm font-medium">
Nome <span className="text-red-500">*</span>
</label>
<input
id="name"
{...register("name")}
aria-invalid={!!errors.name}
aria-describedby={errors.name ? "name-err" : undefined}
className={`w-full rounded border p-2 ${errors.name ? "border-red-500" : ""}`}
/>
{errors.name && <FieldError message={errors.name.message} />}
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium">
Email <span className="text-red-500">*</span>
</label>
<input
id="email"
{...register("email")}
aria-invalid={!!errors.email}
className={`w-full rounded border p-2 ${errors.email ? "border-red-500" : ""}`}
/>
<FieldError message={errors.email?.message} />
</div>
<div>
<label htmlFor="phone" className="block text-sm font-medium">Telefone</label>
<input
id="phone"
{...register("phone")}
aria-invalid={!!errors.phone}
className={`w-full rounded border p-2 ${errors.phone ? "border-red-500" : ""}`}
/>
<FieldError message={errors.phone?.message} />
</div>
<div>
<label htmlFor="msg" className="block text-sm font-medium">
Mensagem <span className="text-red-500">*</span>
</label>
<textarea
id="msg"
{...register("message")}
rows={4}
aria-invalid={!!errors.message}
className={`w-full rounded border p-2 ${errors.message ? "border-red-500" : ""}`}
/>
<FieldError message={errors.message?.message} />
</div>
<button
type="submit"
disabled={isSubmitting}
className="w-full rounded bg-blue-600 px-4 py-2 text-white disabled:opacity-50"
>
{isSubmitting ? "Enviando..." : "Enviar Mensagem"}
</button>
</form>
{toast && <Toast message={toast} onClose={() => setToast(null)} />}
</>
);
}O que isso demonstra:
setError("root")aria-invalid e role="alert" para acessibilidadeerrors do RHF espelha a forma do esquema do formulário - acesse errors.fieldName.messagesetError("root", ...) define um erro não relacionado a campo, acessível via errors.rootrole="alert" faz com que leitores de tela anunciem o erro imediatamentearia-invalid={true} marca o input como inválido para tecnologia assistivaaria-describedby vincula o input ao seu elemento de mensagem de errotabIndex={-1} e focus() garante que usuários de teclado notem os errosToast com Sonner:
import { toast } from "sonner";
async function onSubmit(data: FormData) {
try {
await submitForm(data);
toast.success("Salvo com sucesso!");
} catch (err) {
toast.error("Algo deu errado", { description: err.message });
}
}Error boundary para erros inesperados:
function FormErrorBoundary({ children }: { children: React.ReactNode }) {
return (
<ErrorBoundary
fallback={
<div role="alert" className="rounded bg-red-50 p-4 text-red-800">
Ocorreu um erro. Por favor, atualize e tente novamente.
</div>
}
>
{children}
</ErrorBoundary>
);
}Dicas inline que se tornam erros:
function PasswordField({ register, error }: { register: any; error?: FieldError }) {
const value = useWatch({ name: "password" });
const rules = [
{ test: (v: string) => v.length >= 8, label: "8+ caracteres" },
{ test: (v: string) => /[A-Z]/.test(v), label: "Letra maiúscula" },
{ test: (v: string) => /\d/.test(v), label: "Número" },
];
return (
<div>
<input {...register("password")} type="password" />
<ul className="mt-1 space-y-0.5 text-xs">
{rules.map((r) => (
<li key={r.label} className={r.test(value || "") ? "text-green-600" : "text-gray-400"}>
{r.test(value || "") ? "check" : "circle"} {r.label}
</li>
))}
</ul>
</div>
);
}// Tipo FieldError do react-hook-form
import type { FieldError } from "react-hook-form";
function ErrorMessage({ error }: { error?: FieldError }) {
if (!error) return null;
return <p className="text-red-600">{error.message}</p>;
}
// Chaves de erro tipadas
type FormErrors = Record<keyof FormData, FieldError | undefined>;Flash de erro na primeira renderização - Se mode: "all", os erros aparecem antes que o usuário interaja. Correção: Use mode: "onBlur" ou mode: "onSubmit" e exiba erros apenas após a primeira interação.
Toast vs inline para validação - Toasts desaparecem, dificultando a localização de qual campo falhou. Correção: Use erros inline para validação; reserve toasts para mensagens de sucesso e erros do servidor.
Anúncios de leitores de tela - Múltiplos elementos role="alert" anunciando ao mesmo tempo sobrecarregam os usuários. Correção: Use um único resumo de erros com role="alert" e erros individuais sem ele, ou use aria-live="polite".
Borda de erro sem mensagem - Uma borda vermelha sozinha não ajuda usuários daltônicos. Correção: Sempre combine indicadores visuais com mensagens de texto.
| Alternativa | Use Quando | Não Use Quando |
|---|---|---|
| Validação Nativa HTML | Você só precisa de verificações required e type | Você precisa de mensagens de erro personalizadas ou regras complexas |
| react-hot-toast | Toast leve com dependências mínimas | Você precisa de recursos ricos de toast (promessa, renderização personalizada) |
| Sonner | Você quer toasts bonitos e animados com suporte a promessas | Você precisa de um bundle muito pequeno |
| shadcn Form | Você quer exibição de erro integrada com estilo consistente | Você está construindo um sistema de design personalizado |
setError("root", { message: "Credenciais inválidas" });errors.root?.messagesetError("root", ...) e aparecem em um banner no topotabIndex={-1} torna o div focável via JavaScript, mas não via tecla Tab.focus() programaticamente quando os erros aparecem após o enviomode: "all" valida a cada mudança, incluindo a montagem inicialmode: "onBlur" ou mode: "onSubmit" para adiar a validação até a interaçãorole="alert" aciona um anúncio imediato do leitor de telarole="alert" ou use aria-live="polite" em erros individuaisfunction FieldError({ message }: { message?: string }) {
if (!message) return null;
return <p role="alert" className="text-sm text-red-600">{message}</p>;
}role="alert" para anunciar o erro aos leitores de tela quando ele apareceimport type { FieldError } from "react-hook-form";
function ErrorMessage({ error }: { error?: FieldError }) {
if (!error) return null;
return <p>{error.message}</p>;
}FieldError do react-hook-form para o tipo corretotype FormErrors = Record<keyof FormData, FieldError | undefined>;useEffect define um setTimeout para chamar onClose após 5 segundosrole="status" e aria-live="polite" para anúncios acessíveisimport { toast } from "sonner";
toast.success("Salvo com sucesso!");
toast.error("Algo deu errado", { description: err.message });Revisado por Chris St. John·Última atualização: 10 de jul. de 2026
🤖 Read the SystemsArchitect.io Blog for over 100+ cloud architecture articles 🔥