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
| Attributo | Dove si usa | Scopo |
|---|---|---|
aria-required="true" | Input obbligatori | Comunica ai screen reader che il campo è obbligatorio |
aria-describedby | Input con errore | Collega il campo al messaggio di errore |
aria-invalid="true" | Input non valido | Segnala agli screen reader che il valore è errato |
role="alert" | Span errore | Forza la lettura immediata quando il testo cambia |
autocomplete | Tutti gli input | Aiuta browser e utenti a compilare più velocemente |
Tipi di Input HTML — Riferimento Completo
| Tipo | Uso tipico | Validazione nativa |
|---|---|---|
text | Nome, città, titolo | minlength, maxlength, pattern |
email | Indirizzo email | Formato nome@dominio.ext |
tel | Numero di telefono | Tastiera numerica mobile |
url | Indirizzo web | Deve iniziare con http/https |
number | Quantità, età | min, max, step |
date | Data appuntamento | min, max con formato YYYY-MM-DD |
password | Credenziali | minlength, pattern |
checkbox | Consenso, opzioni multiple | required (almeno uno) |
radio | Scelta singola | required (obbliga selezione) |
file | Upload documento | accept per tipo file |
textarea | Testo lungo | minlength, 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 quando | Attributo HTML correlato |
|---|---|---|
valueMissing | Campo vuoto con required | required |
typeMismatch | Formato sbagliato (email, url) | type |
tooShort | Testo più corto del minimo | minlength |
tooLong | Testo più lungo del massimo | maxlength |
patternMismatch | Non matcha il pattern regex | pattern |
rangeUnderflow | Numero troppo piccolo | min |
rangeOverflow | Numero troppo grande | max |
valid | Tutto 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
| Soluzione | Setup | Costo | Quando usarla |
|---|---|---|---|
| Formspree | Solo action URL | Gratis (50/mese) | Siti statici, portfolio, blog |
| Netlify Forms | Attributo netlify | Gratis (100/mese) | Deploy su Netlify |
| PHP mail() | Script server-side | Hosting richiesto | WordPress, siti con server PHP |
| EmailJS | SDK JS + account | Gratis (200/mese) | Vuoi tutto in JS, no backend |
| Nodemailer | Node.js + SMTP | Server richiesto | App Node.js con backend proprio |
Accessibilità: Checklist Completa
- Ogni
<input>ha un<label>associato tramitefor/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
- MDN — elemento <form> — documentazione completa attributi
- MDN — Constraint Validation API — tutte le proprietà validity
- Formspree — invio form senza backend
- Netlify Forms — alternativa a Formspree per siti Netlify
- W3C WAI — Form Accessibility Tutorial — guida ufficiale accessibilità
- web.dev — Learn Forms — corso completo Google su HTML forms
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.

