Pular para o conteúdo
Cursos

DevClub

LógicaFront-endBack-endMobile

IA Club

IA na prática
Estudar programaçãoEstudar IA
LiçãoIntermediáriocódigo testado

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.

Rodolfo Mori6 min de leitura

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:

js
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.

js
const form = document.querySelector('#cadastro');

form.addEventListener('submit', (evento) => {
  evento.preventDefault();
  console.log('validando...');
  console.log('navegação cancelada:', evento.defaultPrevented);
});
validando... navegação cancelada: true URL continua: https://loja.club/checkout

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:

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:

js
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(', ') || '—');
}
form.checkValidity(): false nome | válido: false | motivo: valueMissing email | válido: false | motivo: typeMismatch senha | válido: false | motivo: tooShort cep | válido: false | motivo: patternMismatch qtd | válido: false | motivo: rangeUnderflow

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:

js
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:

"111.111.111-11" | válido: false | customError: true | mensagem: "Informe um CPF com 11 dígitos." "529.982.247-25" | válido: true | customError: false | mensagem: ""

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. setCustomValidity não valida nada; ele só grava a mensagem. Quem valida é o checkValidity() 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:

js
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:

"a" -> "E-mail inválido." "ana@" -> "E-mail inválido." "ana@loja.club" -> "" submit -> form.checkValidity(): true

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

js
document.querySelector('#aplicar').addEventListener('click', () => {
  console.log('validando o cupom FRETE10...');
});
URL antes : https://loja.club/checkout validando o cupom FRETE10... URL depois: https://loja.club/aplicar-cupom?codigo=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

js
const campos = document.querySelectorAll('#cadastro input');
campos.setCustomValidity('Preencha este campo.');
file:///private/tmp/loja/validacao.mjs:5 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

js
const form = document.querySelector('#cadastro');
console.log(form.elements.celular.value);
file:///private/tmp/loja/envio.mjs:5 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 de submit.
  • required, type, minlength e pattern no HTML — regra declarada é regra que funciona sem JavaScript.
  • novalidate no <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.

  • formulario
  • validacao
  • submit
  • dom
  • checkvalidity

Perguntas frequentes

Preciso de biblioteca para validar formulário?
Para formulário de contato, cadastro ou checkout, não. required, type, minlength e pattern mais a Constraint Validation API cobrem quase tudo. A biblioteca compensa quando há schema compartilhado com o back-end.
O que o atributo novalidate faz exatamente?
Ele desliga só o balão de erro nativo e o bloqueio automático do envio. A API continua funcionando: checkValidity, validity e setCustomValidity respondem igual, e é assim que você assume o controle do visual do erro.
Validar no evento input não incomoda o usuário?
Incomoda quando começa antes da primeira tentativa de envio. O padrão que funciona é validar no submit, e só depois disso ligar a revalidação no input daquele campo — o erro some enquanto a pessoa corrige.
Se o back-end valida, por que validar no front?
Por velocidade de resposta, não por segurança. O front evita uma ida ao servidor para dizer que faltou o CEP. Qualquer pessoa desliga o JavaScript e envia o que quiser, então a validação que protege o dado é a do back.

Dúvidas e comentários

Travou em algum passo? Pergunte aqui — a equipe e outros alunos respondem.

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

  1. MDN — Validação de dados de formulário — developer.mozilla.org
  2. MDN — ValidityState — developer.mozilla.org
  3. HTML Standard — Constraint validation — html.spec.whatwg.org

Continue por aqui