Les formulaires
que le navigateur gère seul
Avant de dégainer une librairie de validation, le HTML natif fait déjà énormément : contraintes, types riches, autocomplétion, messages sur mesure. Six leviers, zéro dépendance.
Validation native — Constraint API
required, type="email",
minlength, pattern : le navigateur
valide sans une ligne de JS, et les pseudo-classes
:valid / :invalid stylent le champ.
Essaie une adresse incomplète.
<form id="form" novalidate>
<label for="email">E-mail (requis)</label>
<input type="email" id="email" required minlength="5" placeholder="prenom@exemple.be">
<button type="submit">Vérifier</button>
<p id="msg" role="status" aria-live="polite"></p>
</form>
input {
padding: .5rem; color: #c9d4e8;
background: #0d1117; border: 1px solid #1e2d44; border-radius: 6px;
}
/* le navigateur reflète l'état via ces pseudo-classes */
input:invalid { border-color: #f87171; }
input:valid { border-color: #4ade80; }
const form = document.getElementById('form');
const email = document.getElementById('email');
const msg = document.getElementById('msg');
form.addEventListener('submit', (e) => {
e.preventDefault();
// checkValidity() + validationMessage : tout est natif, zéro règle à écrire
msg.textContent = email.checkValidity() ? 'Adresse valide ✓' : email.validationMessage;
});
Types d'input riches
date, color, range,
number, tel… chaque type déclenche le
bon clavier mobile et le bon sélecteur natif — accessibles et
cohérents avec l'OS. Aucune dépendance.
<label>Date <input type="date"></label> <!-- sélecteur de date natif -->
<label>Couleur <input type="color" value="#c9952a"></label> <!-- pipette -->
<label>Curseur <input type="range" min="0" max="100" value="60"></label>
<label>Nombre <input type="number" min="0" max="10" value="3"></label>
<label>Téléphone <input type="tel"></label> <!-- clavier numérique sur mobile -->
datalist — autocomplétion native
Un <datalist> propose des suggestions à un
champ texte, tout en laissant l'utilisateur saisir librement.
Zéro JS, zéro plugin d'autocomplétion. Tape « J » ci-dessous.
<input list="technos" placeholder="Commence à taper…">
<datalist id="technos">
<option value="JavaScript">
<option value="CSS">
<option value="SVG">
<option value="HTML">
<option value="WebAssembly">
</datalist>
<!-- suggestions natives, saisie libre conservée, zéro JS -->
Validation personnalisée
setCustomValidity() branche tes propres règles sur
le mécanisme natif : le champ devient :invalid,
le message s'affiche comme un message natif. Ici : le mot doit
contenir « owl ».
<form id="form" novalidate>
<label for="pw">Mot de passe (doit contenir « owl »)</label>
<input type="text" id="pw" placeholder="super-owl-2026">
<button type="submit">Valider</button>
<p id="msg" role="status" aria-live="polite"></p>
</form>
const input = document.getElementById('pw');
const msg = document.getElementById('msg');
function check() {
// branche une règle perso sur le mécanisme natif (le champ devient :invalid)
input.setCustomValidity(input.value.includes('owl') ? '' : 'Le mot doit contenir « owl ».');
}
input.addEventListener('input', check);
document.getElementById('form').addEventListener('submit', (e) => {
e.preventDefault();
check();
msg.textContent = input.checkValidity() ? 'Parfait, « owl » est bien là ✓' : input.validationMessage;
});
:user-valid / :user-invalid
:invalid colore le champ en rouge avant même
toute saisie. Les pseudo-classes modernes
:user-valid / :user-invalid n'agissent
qu'après interaction — bien plus agréable. Compare.
<label>:invalid (rouge d'emblée)
<input class="eager" type="email" required placeholder="requis">
</label>
<label>:user-invalid (après saisie)
<input class="lazy" type="email" required placeholder="requis">
</label>
input {
padding: .5rem; color: #c9d4e8;
background: #0d1117; border: 1px solid #1e2d44; border-radius: 6px;
}
/* rouge dès le chargement — trop agressif */
.eager:invalid { border-color: #f87171; }
/* rouge seulement APRÈS que l'utilisateur a touché le champ */
.lazy:user-invalid { border-color: #f87171; }
.lazy:user-valid { border-color: #4ade80; }
FormData & soumission sans rechargement
new FormData(form) sérialise tout le formulaire en
un objet prêt pour fetch() — envoi asynchrone, sans
rechargement de page. Remplis et soumets pour voir les données
collectées.
<form id="form">
<label>Nom <input name="nom" value="OWL"></label>
<label>Rôle <input name="role" value="Studio digital"></label>
<button type="submit">Sérialiser</button>
<pre id="output"></pre>
</form>
document.getElementById('form').addEventListener('submit', (e) => {
e.preventDefault();
const data = new FormData(e.target); // sérialise tout le formulaire
const obj = Object.fromEntries(data); // { nom: 'OWL', role: '…' }
document.getElementById('output').textContent = JSON.stringify(obj, null, 2);
// fetch('/api', { method: 'POST', body: data }); // envoi async, sans rechargement
});