Validar formulário com JavaScript: guia prático
Submit com preventDefault, checkValidity, o objeto validity e setCustomValidity para regras próprias — com o motivo real de cada campo inválido no terminal.
Validar formulário em JavaScript é escutar o evento submit, cancelar o envio
padrão com preventDefault e só então decidir se os dados servem. O esqueleto
inteiro cabe em cinco linhas:
const form = document.querySelector('#cadastro');
form.addEventListener('submit', (evento) => {
evento.preventDefault();
if (form.checkValidity()) enviarPedido();
});O que quase ninguém aproveita é que o navegador já validou tudo antes de você
chegar. required, type="email", minlength, pattern, min e max viram
um objeto validity em cada campo, com o motivo exato da recusa. Escrever regex
de e-mail à mão em 2026 é refazer trabalho que o navegador entrega pronto.
JavaScript é o atendente; o HTML traz a checklist
Num balcão de cadastro, a checklist já diz que e-mail é obrigatório e senha precisa de um tamanho mínimo. O atendente não precisa inventar essas regras: ele usa a lista, explica o problema no momento certo e impede que a ficha siga incompleta. No formulário, o HTML declara as restrições e o JavaScript melhora a conversa com a pessoa.
O evento técnico que reúne essa decisão é o submit do formulário, não o
clique de um botão específico. checkValidity() dá o veredito e validity
explica o motivo campo por campo. Desative o JavaScript por um minuto e envie o
formulário: as regras básicas ainda devem funcionar. Depois reative e observe o
que foi acrescentado. Se toda validação desaparece sem seu script, você refez a
checklist em vez de aproveitar a camada que o navegador já entrega.
O submit é o gancho, e ele é do <form>
Escute o submit no formulário, não o click no botão. São eventos diferentes:
o submit também dispara quando o usuário aperta Enter no campo de texto, e o
click não.
const form = document.querySelector('#cadastro');
form.addEventListener('submit', (evento) => {
evento.preventDefault();
console.log('validando...');
console.log('navegação cancelada:', evento.defaultPrevented);
});preventDefault() vai na primeira linha do handler. Se ele ficar depois de
um if, existe um caminho em que a página recarrega — e você vai gastar uma
tarde procurando por quê. É o mesmo cancelamento explicado em
objeto event e preventDefault.
O que o navegador já valida sozinho
Antes de escrever qualquer regra, declare as que são do HTML:
<form id="cadastro" novalidate>
<input name="nome" required>
<input name="email" type="email" required>
<input name="senha" type="password" minlength="8">
<input name="cep" pattern="\d{5}-\d{3}">
<input name="qtd" type="number" min="1" max="10">
<button>Criar conta</button>
</form>O novalidate no <form> desliga o balão amarelo nativo e o bloqueio automático
do envio — mas não desliga a validação. O objeto validity continua
funcionando, e é isso que permite mostrar o erro do seu jeito, no seu CSS.
validity: o motivo, não só o veredito
Cada campo carrega um ValidityState com uma flag por tipo de violação.
Percorrendo o formulário depois de o usuário digitar todos os valores errados de
propósito — nome em branco, ana no e-mail, 123 na senha, 1234 no CEP e 0
na quantidade:
const form = document.querySelector('#cadastro');
console.log('form.checkValidity():', form.checkValidity());
for (const campo of form.elements) {
if (!campo.name) continue;
const v = campo.validity;
const motivos = ['valueMissing', 'typeMismatch', 'tooShort', 'patternMismatch',
'rangeUnderflow', 'rangeOverflow'].filter((k) => v[k]);
console.log(campo.name.padEnd(6), '| válido:', String(campo.checkValidity()).padEnd(5),
'| motivo:', motivos.join(', ') || '—');
}Cinco campos, cinco motivos diferentes, zero regex escrita por você. É essa flag que vira a mensagem na tela:
flag de validity |
disparada por | mensagem típica |
|---|---|---|
valueMissing |
required com campo vazio |
“Informe o seu nome.” |
typeMismatch |
type="email" ou type="url" fora do formato |
“E-mail inválido.” |
tooShort / tooLong |
minlength / maxlength |
“A senha precisa de 8 caracteres.” |
patternMismatch |
pattern não casou |
“Use o formato 01310-100.” |
rangeUnderflow / rangeOverflow |
min / max |
“Quantidade entre 1 e 10.” |
stepMismatch |
step não bate |
“Use múltiplos de 5.” |
customError |
você chamou setCustomValidity |
a sua mensagem |
valid |
nenhuma das acima | — |
campo.checkValidity() devolve true/false e dispara o evento invalid no
campo. form.checkValidity() faz o mesmo para o formulário todo. E
form.reportValidity() faz a mesma checagem e mostra o balão nativo no
primeiro campo inválido — útil quando você não tem novalidate.
setCustomValidity: as regras que o HTML não conhece
CPF, CNPJ, “as duas senhas conferem”, “o cupom existe”. Você calcula e informa o resultado ao navegador:
const cpf = document.querySelector('#cpf');
function digitosValidos(valor) {
const numeros = valor.replace(/\D/g, '');
return numeros.length === 11 && !/^(\d)\1{10}$/.test(numeros);
}
function validarCpf() {
cpf.setCustomValidity(digitosValidos(cpf.value) ? '' : 'Informe um CPF com 11 dígitos.');
console.log(JSON.stringify(cpf.value), '| válido:', cpf.checkValidity(),
'| customError:', cpf.validity.customError,
'| mensagem:', JSON.stringify(cpf.validationMessage));
}Com 111.111.111-11 e depois com um CPF de verdade:
Duas coisas que derrubam quem usa setCustomValidity pela primeira vez:
- String vazia é o único jeito de limpar. Enquanto a mensagem não for
'', o campo continua inválido para sempre — inclusive depois de o usuário corrigir. - Chame antes de checar.
setCustomValiditynão valida nada; ele só grava a mensagem. Quem valida é ocheckValidity()da linha seguinte.
Quando mostrar o erro: submit primeiro, input depois
Validar a cada tecla desde o primeiro caractere transforma o cadastro numa
sequência de acusações — a pessoa digita a e já leva “e-mail inválido”. O
padrão que funciona é validar no submit e, a partir daí, revalidar aquele campo
no input:
const email = document.querySelector('#email');
const saida = document.querySelector('#erro-email');
function validarEmail() {
if (email.validity.valid) { saida.textContent = ''; return true; }
if (email.validity.valueMissing) saida.textContent = 'Informe o e-mail.';
else if (email.validity.typeMismatch) saida.textContent = 'E-mail inválido.';
return false;
}
email.addEventListener('input', validarEmail);Digitando a, depois ana@, depois ana@loja.club, e enviando:
O erro apareceu, ficou enquanto o valor estava errado e sumiu sozinho quando
ficou certo. Note que quem escreve na tela é o seu código: validity é o dado,
o <span> é a interface. Como o handler lê o valor pelo próprio campo, ele se
encaixa direto no que você viu em
pegar o valor de um input.
Erros comuns
O formulário envia antes de a validação rodar
document.querySelector('#aplicar').addEventListener('click', () => {
console.log('validando o cupom FRETE10...');
});A validação rodou — e o navegador enviou o formulário logo depois, porque
<button> dentro de <form> é type="submit" por padrão e ninguém cancelou
nada. Não há erro no console; o sintoma é a página piscando e voltando ao
estado inicial. Duas correções, e você quer as duas: escute submit no <form>
e chame preventDefault().
setCustomValidity is not a function
const campos = document.querySelectorAll('#cadastro input');
campos.setCustomValidity('Preencha este campo.');TypeError: campos.setCustomValidity is not a function at file:///private/tmp/loja/validacao.mjs:5:8 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
querySelectorAll devolve uma NodeList, não um campo. Métodos de elemento
moram nos elementos: campos.forEach((campo) => campo.setCustomValidity(...)).
O s no fim do nome da variável é o aviso que estava ali o tempo todo.
O campo que você acha que existe
const form = document.querySelector('#cadastro');
console.log(form.elements.celular.value);TypeError: Cannot read properties of undefined (reading ‘value’) at file:///private/tmp/loja/envio.mjs:5:35 at ModuleJob.run (node:internal/modules/esm/module_job:439:25)
Node.js v24.16.0
O HTML tem name="telefone"; o JavaScript pediu celular. form.elements
devolve undefined para nome que não existe, e undefined.value estoura. É a
mesma família de
TypeError: Cannot read properties of undefined,
com a agravante de que o name costuma ser decidido pelo back-end e mudar sem
avisar o front.
Validação no front não é validação
Ela é interface. Serve para o usuário descobrir em 50 milissegundos que faltou o
CEP, em vez de descobrir depois de uma ida ao servidor. Quem garante que o dado
está bom é o back-end, e não existe atalho: qualquer pessoa abre o DevTools,
remove o required e envia.
Um checklist para fechar o formulário:
preventDefault()na primeira linha do handler desubmit.required,type,minlengthepatternno HTML — regra declarada é regra que funciona sem JavaScript.novalidateno<form>só quando você for desenhar as mensagens.setCustomValidity('')em todo caminho de sucesso.- A mesma regra repetida no servidor, sempre.
- Botão de envio desabilitado depois do primeiro submit válido, para não duplicar pedido.
A próxima lição guarda o que o formulário produziu para o usuário não perder o rascunho: localStorage: salvar dados no navegador. O guia completo de JavaScript mostra a trilha inteira.
Perguntas frequentes
Preciso de biblioteca para validar formulário?
O que o atributo novalidate faz exatamente?
Validar no evento input não incomoda o usuário?
Se o back-end valida, por que validar no front?
Dúvidas e comentários
Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.
Entrar para perguntarÉ o mesmo login gratuito dos cursos.
Nenhuma dúvida por aqui ainda — a primeira pode ser a sua.
Todo o código deste artigo foi executado em Node 24.16.0, e as saídas exibidas são as reais — como produzimos este conteúdo.
Fontes consultadas
- MDN — Validação de dados de formulário — developer.mozilla.org
- MDN — ValidityState — developer.mozilla.org
- HTML Standard — Constraint validation — html.spec.whatwg.org


