Come creare un modulo HTML con convalida lato client in JavaScript

Un modulo ben fatto non è solo “campi + invio”: deve guidare l’utente, prevenire errori e validare i dati in modo chiaro. In questa guida creiamo un form HTML con convalida lato client in JavaScript usando la Constraint Validation API, messaggi personalizzati e buone pratiche di UX e accessibilità, con codice pronto da copiare.

Programmazione JavaScript
Programmazione JavaScript

La convalida lato client serve a migliorare l’esperienza dell’utente: segnala subito gli errori, riduce invii inutili e rende il form più “usabile”. Ma non è una misura di sicurezza: ogni controllo va comunque ripetuto lato server. Detto questo, una validazione client fatta bene è uno di quei dettagli che cambia davvero le conversioni (e riduce i contatti “sporchi”).

In questa guida vediamo come creare un modulo HTML moderno con convalida lato client in JavaScript, usando gli strumenti nativi del browser (Constraint Validation API) e aggiungendo messaggi personalizzati, gestione degli errori, accessibilità e una UX pulita.

Cosa significa “convalida lato client”

Convalida lato client significa verificare i dati prima che il form venga inviato al server. Esempi:

  • campo email vuoto o non valido
  • password troppo corta
  • checkbox “accetto la privacy” non selezionata
  • numero di telefono con formato errato

Il modo migliore per farla è combinare:

  • validazione HTML5 (required, type, minlength, pattern, ecc.)
  • JavaScript per gestire UI, messaggi personalizzati e logica extra

Regola d’oro: client-side UX, server-side sicurezza

Ricorda: l’utente può disabilitare JS, modificare l’HTML o inviare richieste direttamente via API. Quindi:

  • la validazione client serve per UX e qualità dati
  • la validazione server serve per sicurezza e integrità

Esempio completo: form con validazione e messaggi personalizzati

Qui sotto trovi un esempio pronto: nome, email, telefono, messaggio e consenso privacy. Usiamo:

  • Constraint Validation API (checkValidity, reportValidity, validity)
  • messaggi errori per singolo campo
  • classi CSS per stato valido/non valido
  • accessibilità con aria-describedby e aria-live
<form id="contactForm" novalidate>
  <div class="field">
    <label for="name">Nome e cognome</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">Telefono (opzionale)</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 ammesso: numeri, spazi e trattini.</small>
    <p id="phoneError" class="error" aria-live="polite"></p>
  </div>

  <div class="field">
    <label for="message">Messaggio</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">
      Ho letto e accetto l’informativa privacy
    </label>
    <p id="privacyError" class="error" aria-live="polite"></p>
  </div>

  <button type="submit">Invia</button>

  <p id="formStatus" class="status" aria-live="polite"></p>
</form>

Stili minimi (facoltativi) per rendere chiari gli stati:

.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 ora la parte JavaScript: validazione al submit + validazione “live” durante la digitazione.

(() => {
  const form = document.getElementById('contactForm');
  const statusEl = document.getElementById('formStatus');

  // Mappa campo -> elemento errori
  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) {
      // Messaggi specifici per campo
      if (input.name === 'privacy') return 'Devi accettare l’informativa privacy per proseguire.';
      return 'Questo campo è obbligatorio.';
    }
    if (v.typeMismatch) {
      if (input.type === 'email') return 'Inserisci un indirizzo email valido.';
      return 'Formato non valido.';
    }
    if (v.tooShort) {
      return `Inserisci almeno ${input.minLength} caratteri.`;
    }
    if (v.patternMismatch) {
      if (input.name === 'phone') return 'Inserisci un numero valido (numeri, spazi e trattini).';
      return 'Formato non valido.';
    }
    if (v.badInput) return 'Valore non valido.';
    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);

    // Per accessibilità: aria-invalid quando non valido
    input.setAttribute('aria-invalid', String(!isValid));
  };

  const validateField = (input) => {
    // telefono opzionale: se vuoto, consideralo valido
    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 });
  };

  // Validazione live: al blur e all'input
  form.addEventListener('blur', (e) => {
    const target = e.target;
    if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
      validateField(target);
    }
  }, true);

  // Debounce semplice per non validare a ogni tasto in modo aggressivo
  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 = 'Controlla i campi evidenziati.';
      focusFirstInvalid();
      return;
    }

    // Qui invieresti i dati al server (fetch/AJAX) oppure lasci fare al form
    // Esempio placeholder:
    statusEl.textContent = 'Invio in corso...';

    try {
      // Simulazione invio
      await new Promise((r) => setTimeout(r, 400));
      statusEl.textContent = 'Messaggio inviato correttamente.';
      form.reset();

      // Pulisci stati visivi dopo 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 = 'Errore durante l’invio. Riprova.';
    }
  });
})();

Perché usare la Constraint Validation API

Molti reinventano la ruota con regex ovunque e controlli manuali. In realtà i browser moderni offrono un sistema robusto già pronto:

  • checkValidity() verifica la validità del campo senza mostrare popup del browser
  • reportValidity() può mostrare i messaggi nativi (utile in casi semplici)
  • validity espone lo stato dettagliato (valueMissing, tooShort, typeMismatch, patternMismatch…)

Così ti concentri sulla UX (messaggi, evidenziazione, accessibilità), senza duplicare logiche inutili.

Validazione “gentile”: quando mostrare l’errore

Un errore mostrato troppo presto può infastidire. Linee guida pratiche:

  • mostra gli errori al submit (sempre)
  • mostra gli errori al blur (quando l’utente esce dal campo)
  • valida “live” durante la digitazione solo con debounce e solo se il campo è già stato toccato (approccio più avanzato)

Nell’esempio, al submit validiamo tutto; durante la compilazione validiamo al blur e con un debounce leggero.

Accessibilità: non è opzionale

Due attenzioni fanno la differenza:

  • collega il testo d’errore al campo con aria-describedby
  • aggiorna lo stato con aria-invalid quando necessario

In più, usare aria-live="polite" sul testo errore permette ai lettori di schermo di comunicare il cambiamento.

FAQ rapide

Devo usare novalidate nel form?

Se vuoi gestire tu messaggi e UI, sì: novalidate disabilita i popup nativi del browser e ti permette una UX coerente. Se ti basta la validazione HTML5 standard, puoi anche non usarlo.

La regex del telefono è obbligatoria?

No. I numeri di telefono sono complessi (formati diversi). Meglio una validazione permissiva lato client e una normalizzazione lato server, oppure librerie dedicate quando serve alta precisione.

Posso inviare con fetch senza ricaricare la pagina?

Sì. Nel blocco submit trovi già un punto in cui inserire fetch(). Ricorda: validazione server sempre, gestione errori e messaggi chiari lato client.

Conclusione

Un modulo con validazione lato client in JavaScript non deve essere complicato: HTML5 ti dà già regole solide, JavaScript ti permette di renderle più chiare, coerenti e accessibili. Se costruisci messaggi utili, evidenzi gli errori nel modo giusto e non dimentichi l’accessibilità, ottieni form più veloci, meno frustrazione e dati di qualità migliore.

Pubblicato in ,

Se vuoi rimanere aggiornato su Come creare un modulo HTML con convalida lato client in JavaScript iscriviti alla nostra newsletter settimanale

Commenta per primo

Lascia un commento

L'indirizzo email non sarà pubblicato.


*