A validação do lado do cliente serve para melhorar a experiência do usuário: ela sinaliza erros imediatamente, reduz envios desnecessários e torna o formulário mais “utilizável”. Mas não é uma medida de segurança: cada controle deve ser repetido no lado do servidor. Dito isso, uma validação de cliente bem feita é um daqueles detalhes que realmente muda as conversões (e reduz contatos “sujos”).
Neste guia, veremos como criar um formulário HTML moderno com validação do lado do cliente em JavaScript, usando as ferramentas nativas do navegador (Constraint Validation API) e adicionando mensagens personalizadas, gerenciamento de erros, acessibilidade e uma UX limpa.
O que significa “validação do lado do cliente”
Validação do lado do cliente significa verificar os dados antes que o formulário seja enviado ao servidor. Exemplos:
- campo de e-mail vazio ou inválido
- senha muito curta
- checkbox “aceito a política de privacidade” não selecionado
- número de telefone com formato incorreto
A melhor forma de fazer isso é combinando:
- validação HTML5 (required, type, minlength, pattern, etc.)
- JavaScript para gerenciar UI, mensagens personalizadas e lógica extra
Regra de ouro: UX do lado do cliente, segurança do lado do servidor
Lembre-se: o usuário pode desabilitar o JS, modificar o HTML ou enviar requisições diretamente via API. Então:
- a validação do cliente serve para UX e qualidade dos dados
- a validação do servidor serve para segurança e integridade
Exemplo completo: formulário com validação e mensagens personalizadas
Abaixo você encontra um exemplo pronto: nome, e-mail, telefone, mensagem e consentimento de privacidade. Usamos:
- Constraint Validation API (checkValidity, reportValidity, validity)
- mensagens de erro por campo individual
- classes CSS para estado válido/inválido
- acessibilidade com aria-describedby e aria-live
<form id="contactForm" novalidate>
<div class="field">
<label for="name">Nome e sobrenome</label>
<input id="name" name="name" type="text" required minlength="2"
autocomplete="name" aria-describedby="nameError">
<p id="nameError" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="email">Email</label>
<input id="email" name="email" type="email" required
autocomplete="email" aria-describedby="emailError">
<p id="emailError" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="phone">Telefone (opcional)</label>
<input id="phone" name="phone" type="tel"
inputmode="tel"
pattern="^+?[0-9s-]{7,20}$"
placeholder="+39 348 123 4567"
autocomplete="tel" aria-describedby="phoneHelp phoneError">
<small id="phoneHelp" class="help">Formato permitido: números, espaços e hífens.</small>
<p id="phoneError" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="message">Mensagem</label>
<textarea id="message" name="message" required minlength="10" rows="5"
aria-describedby="messageError"></textarea>
<p id="messageError" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label class="checkbox">
<input id="privacy" name="privacy" type="checkbox" required aria-describedby="privacyError">
Li e aceito a política de privacidade
</label>
<p id="privacyError" class="error" aria-live="polite"></p>
</div>
<button type="submit">Enviar</button>
<p id="formStatus" class="status" aria-live="polite"></p>
</form>
Estilos mínimos (opcionais) para deixar os estados claros:
.field { margin-bottom: 14px; }
label { display:block; margin-bottom: 6px; font-weight: 600; }
input, textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 8px; }
input.is-invalid, textarea.is-invalid { border-color: #c0392b; }
input.is-valid, textarea.is-valid { border-color: #27ae60; }
.error { margin: 6px 0 0; color: #c0392b; font-size: 0.95rem; }
.help { display:block; margin-top: 6px; color:#666; font-size: 0.9rem; }
.status { margin-top: 12px; }
E agora a parte JavaScript: validação no submit + validação “ao vivo” durante a digitação.
(() => {
const form = document.getElementById('contactForm');
const statusEl = document.getElementById('formStatus');
// Mapeia campo -> elemento de erro
const errorMap = new Map([
['name', document.getElementById('nameError')],
['email', document.getElementById('emailError')],
['phone', document.getElementById('phoneError')],
['message', document.getElementById('messageError')],
['privacy', document.getElementById('privacyError')],
]);
const getErrorMessage = (input) => {
const v = input.validity;
if (v.valueMissing) {
// Mensagens específicas por campo
if (input.name === 'privacy') return 'Você deve aceitar a política de privacidade para prosseguir.';
return 'Este campo é obrigatório.';
}
if (v.typeMismatch) {
if (input.type === 'email') return 'Digite um endereço de e-mail válido.';
return 'Formato inválido.';
}
if (v.tooShort) {
return `Digite pelo menos ${input.minLength} caracteres.`;
}
if (v.patternMismatch) {
if (input.name === 'phone') return 'Digite um número válido (apenas números, espaços e hífens).';
return 'Formato inválido.';
}
if (v.badInput) return 'Valor inválido.';
return '';
};
const setFieldState = (input, isValid, message = '') => {
const errorEl = errorMap.get(input.name);
if (errorEl) errorEl.textContent = message;
input.classList.toggle('is-invalid', !isValid);
input.classList.toggle('is-valid', isValid);
// Para acessibilidade: aria-invalid quando inválido
input.setAttribute('aria-invalid', String(!isValid));
};
const validateField = (input) => {
// telefone opcional: se vazio, considere-o válido
if (input.name === 'phone' && input.value.trim() === '') {
setFieldState(input, true, '');
return true;
}
const ok = input.checkValidity();
const msg = ok ? '' : getErrorMessage(input);
setFieldState(input, ok, msg);
return ok;
};
const focusFirstInvalid = () => {
const firstInvalid = form.querySelector('.is-invalid');
if (firstInvalid) firstInvalid.focus({ preventScroll: false });
};
// Validação ao vivo: ao blur e ao input
form.addEventListener('blur', (e) => {
const target = e.target;
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
validateField(target);
}
}, true);
// Debounce simples para não validar a cada tecla de forma agressiva
let t = null;
form.addEventListener('input', (e) => {
const target = e.target;
if (!(target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement)) return;
clearTimeout(t);
t = setTimeout(() => validateField(target), 200);
});
form.addEventListener('submit', async (e) => {
e.preventDefault();
statusEl.textContent = '';
const inputs = Array.from(form.querySelectorAll('input, textarea'));
const allOk = inputs.every(validateField);
if (!allOk) {
statusEl.textContent = 'Verifique os campos destacados.';
focusFirstInvalid();
return;
}
// Aqui você enviaria os dados para o servidor (fetch/AJAX) ou deixaria o formulário fazer
// Exemplo placeholder:
statusEl.textContent = 'Enviando...';
try {
// Simulação de envio
await new Promise((r) => setTimeout(r, 400));
statusEl.textContent = 'Mensagem enviada com sucesso.';
form.reset();
// Limpa estados visuais após o reset
inputs.forEach((i) => {
i.classList.remove('is-valid', 'is-invalid');
i.removeAttribute('aria-invalid');
const err = errorMap.get(i.name);
if (err) err.textContent = '';
});
} catch (err) {
statusEl.textContent = 'Erro ao enviar. Tente novamente.';
}
});
})();
Por que usar a Constraint Validation API
Muitos reinventam a roda com regex em todo lugar e controles manuais. Na verdade, os navegadores modernos oferecem um sistema robusto já pronto:
checkValidity()verifica a validade do campo sem mostrar popups do navegadorreportValidity()pode mostrar as mensagens nativas (útil em casos simples)validityexpõe o estado detalhado (valueMissing, tooShort, typeMismatch, patternMismatch…)
Assim, você se concentra na UX (mensagens, destaque, acessibilidade), sem duplicar lógicas inúteis.
Validação “gentil”: quando mostrar o erro
Um erro exibido muito cedo pode incomodar. Diretrizes práticas:
- mostre os erros no submit (sempre)
- mostre os erros no blur (quando o usuário sai do campo)
- valide “ao vivo” durante a digitação apenas com debounce e apenas se o campo já foi tocado (abordagem mais avançada)
No exemplo, no submit validamos tudo; durante o preenchimento validamos no blur e com um debounce leve.
Acessibilidade: não é opcional
Duas atenções fazem a diferença:
- conecte o texto de erro ao campo com
aria-describedby - atualize o estado com
aria-invalidquando necessário
Além disso, usar aria-live="polite" no texto de erro permite que os leitores de tela comuniquem a mudança.
FAQs rápidas
Devo usar novalidate no formulário?
Se você quiser gerenciar suas mensagens e UI, sim: novalidate desabilita os popups nativos do navegador e permite uma UX consistente. Se a validação HTML5 padrão for suficiente, você pode até não usá-lo.
A regex do telefone é obrigatória?
Não. Os números de telefone são complexos (formatos diferentes). Melhor uma validação permissiva do lado do cliente e uma normalização do lado do servidor, ou bibliotecas dedicadas quando for necessária alta precisão.
Posso enviar com fetch sem recarregar a página?
Sim. No bloco de submit você já encontra um ponto onde inserir fetch(). Lembre-se: validação do servidor sempre, gerenciamento de erros e mensagens claras no lado do cliente.
Conclusão
Um formulário com validação do lado do cliente em JavaScript não precisa ser complicado: HTML5 já oferece regras sólidas, JavaScript permite torná-las mais claras, consistentes e acessíveis. Se você construir mensagens úteis, destacar os erros da maneira certa e não esquecer a acessibilidade, obterá formulários mais rápidos, menos frustração e dados de melhor qualidade.
Pubblicato in HTML & CSS, JavaScript
Seja o primeiro a comentar