Come Creare un Form di Contatto in HTML e CSS

Un form di contatto è uno degli elementi più importanti di qualsiasi sito web. In questo articolo costruiamo un form completo passo per passo: HTML semantico, CSS moderno, validazione nativa con la Constraint Validation API, attributi ARIA per l’accessibilità e un esempio funzionante con Formspree — zero backend, zero server.

Struttura HTML del Form di Contatto

Il markup corretto usa label associati, aria-describedby per i messaggi di errore e aria-required per i campi obbligatori.

<form class="contact-form" id="contactForm" novalidate>

  <div class="form-field">
    <label for="name">Nome *</label>
    <input
      type="text"
      id="name"
      name="name"
      autocomplete="name"
      aria-required="true"
      aria-describedby="name-error"
      placeholder="Mario Rossi"
    >
    <span class="error-msg" id="name-error" role="alert"></span>
  </div>

  <div class="form-field">
    <label for="email">Email *</label>
    <input
      type="email"
      id="email"
      name="email"
      autocomplete="email"
      aria-required="true"
      aria-describedby="email-error"
      placeholder="mario@esempio.it"
    >
    <span class="error-msg" id="email-error" role="alert"></span>
  </div>

  <div class="form-field">
    <label for="message">Messaggio *</label>
    <textarea
      id="message"
      name="message"
      rows="5"
      aria-required="true"
      aria-describedby="message-error"
      placeholder="Scrivi qui il tuo messaggio..."
    ></textarea>
    <span class="error-msg" id="message-error" role="alert"></span>
  </div>

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

</form>

Attributi ARIA fondamentali

AttributoDove si usaScopo
aria-required="true"Input obbligatoriComunica ai screen reader che il campo è obbligatorio
aria-describedbyInput con erroreCollega il campo al messaggio di errore
aria-invalid="true"Input non validoSegnala agli screen reader che il valore è errato
role="alert"Span erroreForza la lettura immediata quando il testo cambia
autocompleteTutti gli inputAiuta browser e utenti a compilare più velocemente

Tipi di Input HTML — Riferimento Completo

TipoUso tipicoValidazione nativa
textNome, città, titolominlength, maxlength, pattern
emailIndirizzo emailFormato nome@dominio.ext
telNumero di telefonoTastiera numerica mobile
urlIndirizzo webDeve iniziare con http/https
numberQuantità, etàmin, max, step
dateData appuntamentomin, max con formato YYYY-MM-DD
passwordCredenzialiminlength, pattern
checkboxConsenso, opzioni multiplerequired (almeno uno)
radioScelta singolarequired (obbliga selezione)
fileUpload documentoaccept per tipo file
textareaTesto lungominlength, maxlength

CSS: Stile Moderno con Feedback Visivo

.contact-form {
  background: #fff;
  padding: 2rem;
  border-radius: 12px;
  box-shadow: 0 4px 24px rgba(0,0,0,.08);
  max-width: 480px;
  width: 100%;
}

.form-field {
  display: flex;
  flex-direction: column;
  gap: .4rem;
  margin-bottom: 1.25rem;
}

label {
  font-weight: 600;
  font-size: .9rem;
  color: #374151;
}

input, textarea {
  padding: .65rem .9rem;
  border: 1.5px solid #d1d5db;
  border-radius: 8px;
  font-size: 1rem;
  transition: border-color .2s, box-shadow .2s;
  width: 100%;
  box-sizing: border-box;
}

input:focus, textarea:focus {
  outline: none;
  border-color: #6366f1;
  box-shadow: 0 0 0 3px rgba(99,102,241,.15);
}

/* Stato errore */
input[aria-invalid="true"],
textarea[aria-invalid="true"] {
  border-color: #ef4444;
}

.error-msg {
  font-size: .8rem;
  color: #ef4444;
  min-height: 1.1em;
}

button[type="submit"] {
  background: #6366f1;
  color: #fff;
  border: none;
  padding: .75rem 1.5rem;
  border-radius: 8px;
  font-size: 1rem;
  font-weight: 600;
  cursor: pointer;
  width: 100%;
  transition: background .2s;
}

button[type="submit"]:hover {
  background: #4f46e5;
}

button[type="submit"]:disabled {
  background: #a5b4fc;
  cursor: not-allowed;
}

Validazione con la Constraint Validation API

La Constraint Validation API è la soluzione nativa del browser: niente librerie, niente regex scritte a mano. Ogni input espone validity, validationMessage e il metodo setCustomValidity(). La chiave è usare novalidate sul <form> per disabilitare il popup nativo del browser e gestire tutto con JS — così mantieni controllo completo su design e messaggi.

const form = document.getElementById('contactForm');

// Messaggi di errore personalizzati
const errorMessages = {
  name: {
    valueMissing:  'Il nome è obbligatorio.',
    tooShort:      'Il nome deve avere almeno 2 caratteri.',
  },
  email: {
    valueMissing:  "L'email è obbligatoria.",
    typeMismatch:  'Inserisci un indirizzo email valido (es. mario@esempio.it).',
  },
  message: {
    valueMissing:  'Il messaggio è obbligatorio.',
    tooShort:      'Il messaggio deve avere almeno 10 caratteri.',
  },
};

function getErrorMessage(field) {
  const msgs = errorMessages[field.name] || {};
  const v = field.validity;
  if (v.valueMissing)  return msgs.valueMissing  || 'Campo obbligatorio.';
  if (v.typeMismatch)  return msgs.typeMismatch  || 'Formato non valido.';
  if (v.tooShort)      return msgs.tooShort      || 'Testo troppo corto.';
  return field.validationMessage; // fallback messaggio browser
}

function validateField(field) {
  const errorEl = document.getElementById(field.id + '-error');
  if (!field.validity.valid) {
    field.setAttribute('aria-invalid', 'true');
    errorEl.textContent = getErrorMessage(field);
  } else {
    field.removeAttribute('aria-invalid');
    errorEl.textContent = '';
  }
}

// Validazione in tempo reale (solo dopo primo tentativo submit)
let submitted = false;
form.querySelectorAll('input, textarea').forEach(field => {
  field.addEventListener('input', () => {
    if (submitted) validateField(field);
  });
  field.addEventListener('blur', () => {
    if (submitted) validateField(field);
  });
});

form.addEventListener('submit', e => {
  e.preventDefault();
  submitted = true;

  const fields = [...form.querySelectorAll('input, textarea')];
  fields.forEach(validateField);

  const firstInvalid = fields.find(f => !f.validity.valid);
  if (firstInvalid) {
    firstInvalid.focus(); // focus sul primo campo invalido
    return;
  }

  // Form valido → procedi con l'invio
  console.log('Form valido, invio in corso...');
  // → vedi sezione Formspree sotto
});

Proprietà di validity più utili

ProprietàTrue quandoAttributo HTML correlato
valueMissingCampo vuoto con requiredrequired
typeMismatchFormato sbagliato (email, url)type
tooShortTesto più corto del minimominlength
tooLongTesto più lungo del massimomaxlength
patternMismatchNon matcha il pattern regexpattern
rangeUnderflowNumero troppo piccolomin
rangeOverflowNumero troppo grandemax
validTutto ok

Documentazione completa: MDN — Constraint Validation API.

Inviare il Form Senza Backend: Formspree

Formspree riceve i tuoi form HTML e ti manda una email — niente server, niente PHP. Gratis fino a 50 invii/mese. In alternativa, Netlify Forms funziona allo stesso modo se il sito è deployato su Netlify.

Modifica l’attributo action del form con il tuo endpoint Formspree:

<!-- Sostituisci YOUR_FORM_ID con l'ID che ti dà Formspree -->
<form
  id="contactForm"
  action="https://formspree.io/f/YOUR_FORM_ID"
  method="POST"
  novalidate
>

Poi, nella funzione di submit JavaScript, dopo aver verificato che il form sia valido:

// Dentro il listener submit, dopo il controllo validità:
const submitBtn = form.querySelector('button[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Invio in corso...';

const data = new FormData(form);

fetch(form.action, {
  method: 'POST',
  body: data,
  headers: { 'Accept': 'application/json' },
})
  .then(res => {
    if (res.ok) {
      form.innerHTML = '<p style="color:green;font-weight:600">✓ Messaggio inviato! Ti rispondo entro 24h.</p>';
    } else {
      throw new Error('Errore server');
    }
  })
  .catch(() => {
    submitBtn.disabled = false;
    submitBtn.textContent = 'Invia messaggio';
    alert('Errore durante l'invio. Riprova più tardi.');
  });

Confronto soluzioni invio form

SoluzioneSetupCostoQuando usarla
FormspreeSolo action URLGratis (50/mese)Siti statici, portfolio, blog
Netlify FormsAttributo netlifyGratis (100/mese)Deploy su Netlify
PHP mail()Script server-sideHosting richiestoWordPress, siti con server PHP
EmailJSSDK JS + accountGratis (200/mese)Vuoi tutto in JS, no backend
NodemailerNode.js + SMTPServer richiestoApp Node.js con backend proprio

Accessibilità: Checklist Completa

  • Ogni <input> ha un <label> associato tramite for/id
  • I campi obbligatori hanno aria-required="true"
  • I messaggi di errore usano role="alert" per essere letti subito dagli screen reader
  • Gli input invalidi hanno aria-invalid="true" impostato da JS
  • Il form è navigabile con Tab e inviabile con Enter
  • Il placeholder non sostituisce mai la label
  • Dopo un errore di submit, il focus va sul primo campo invalido
  • Il contrasto colore degli errori è ≥ 4.5:1 (WCAG AA)

Per un approfondimento completo sulle best practice WCAG per i form, leggi la nostra guida su form di contatto accessibili e best practice HTML/ARIA.

CSS: Stile del Form Responsive

Il form è già mobile-first grazie al CSS sopra. Per centrarlo nella pagina:

/* Wrapper centrato */
.form-wrapper {
  display: grid;
  place-items: center;
  min-height: 100vh;
  padding: 2rem;
  background: #f9fafb;
}

Risorse Esterne

Conclusione

Con HTML semantico, CSS moderno e la Constraint Validation API hai tutto il necessario per un form di contatto professionale: niente librerie, niente regex fragili, accessibilità corretta. Formspree risolve il problema del backend in 2 minuti. Il risultato finale è un form che funziona, è accessibile agli screen reader e offre un’ottima UX su qualsiasi dispositivo.

Se stai costruendo interfacce più complesse, esplora anche la guida sulle animazioni CSS con keyframes e transitions per aggiungere micro-interazioni al tuo form.

Hai trovato utile questa guida? Seguimi su Instagram @cyberalchimista per altri tutorial. Per domande o progetti, contattami direttamente.

Articoli correlati

Condividi

Articoli Recenti

Categorie popolari