Como criar um formulário HTML com validação do lado do cliente em JavaScript

Programmazione JavaScript
Programmazione JavaScript

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 navegador
  • reportValidity() pode mostrar as mensagens nativas (útil em casos simples)
  • validity expõ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-invalid quando 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 ,

Se vuoi rimanere aggiornato su Como criar um formulário HTML com validação do lado do cliente em JavaScript iscriviti alla nostra newsletter settimanale

Seja o primeiro a comentar

Faça um comentário

Seu e-mail não será divulgado.


*