La validation côté client a pour but d’améliorer l’expérience utilisateur : elle signale immédiatement les erreurs, réduit les soumissions inutiles et rend le formulaire plus « utilisable ». Mais ce n’est pas une mesure de sécurité : chaque contrôle doit de toute façon être répété côté serveur. Cela dit, une bonne validation côté client est l’un de ces détails qui change vraiment les conversions (et réduit les contacts « sales »).
Dans ce guide, nous allons voir comment créer un formulaire HTML moderne avec validation côté client en JavaScript, en utilisant les outils natifs du navigateur (Constraint Validation API) et en ajoutant des messages personnalisés, une gestion des erreurs, l’accessibilité et une UX propre.
Que signifie « validation côté client »
La validation côté client signifie vérifier les données avant que le formulaire ne soit envoyé au serveur. Exemples :
- champ email vide ou invalide
- mot de passe trop court
- case à cocher « j’accepte la politique de confidentialité » non sélectionnée
- numéro de téléphone avec un format incorrect
La meilleure façon de le faire est de combiner :
- validation HTML5 (required, type, minlength, pattern, etc.)
- JavaScript pour gérer l’interface utilisateur, les messages personnalisés et la logique supplémentaire
Règle d’or : UX côté client, sécurité côté serveur
N’oubliez pas : l’utilisateur peut désactiver JS, modifier l’HTML ou envoyer des requêtes directement via l’API. Donc :
- la validation côté client sert pour l’UX et la qualité des données
- la validation côté serveur sert pour la sécurité et l’intégrité
Exemple complet : formulaire avec validation et messages personnalisés
Vous trouverez ci-dessous un exemple prêt à l’emploi : nom, email, téléphone, message et consentement à la confidentialité. Nous utilisons :
- Constraint Validation API (checkValidity, reportValidity, validity)
- messages d’erreur pour chaque champ
- classes CSS pour l’état valide/invalide
- accessibilité avec aria-describedby et aria-live
<form id="contactForm" novalidate>
<div class="field">
<label for="name">Nom et prénom</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">Téléphone (facultatif)</label>
<input id="phone" name="phone" type="tel"
inputmode="tel"
pattern="^\+?[0-9\s\-]{7,20}$"
placeholder="+39 348 123 4567"
autocomplete="tel" aria-describedby="phoneHelp phoneError">
<small id="phoneHelp" class="help">Format autorisé : chiffres, espaces et tirets.</small>
<p id="phoneError" class="error" aria-live="polite"></p>
</div>
<div class="field">
<label for="message">Message</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">
J’ai lu et j’accepte la politique de confidentialité
</label>
<p id="privacyError" class="error" aria-live="polite"></p>
</div>
<button type="submit">Envoyer</button>
<p id="formStatus" class="status" aria-live="polite"></p>
</form>
Styles minimaux (facultatifs) pour clarifier les états :
.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; }
Et maintenant, la partie JavaScript : validation à la soumission + validation « live » pendant la saisie.
(() => {
const form = document.getElementById('contactForm');
const statusEl = document.getElementById('formStatus');
// Carte champ -> élément d'erreur
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) {
// Messages spécifiques au champ
if (input.name === 'privacy') return 'Vous devez accepter la politique de confidentialité pour continuer.';
return 'Ce champ est obligatoire.';
}
if (v.typeMismatch) {
if (input.type === 'email') return 'Veuillez saisir une adresse email valide.';
return 'Format invalide.';
}
if (v.tooShort) {
return `Veuillez saisir au moins ${input.minLength} caractères.`;
}
if (v.patternMismatch) {
if (input.name === 'phone') return 'Veuillez saisir un numéro valide (chiffres, espaces et tirets).';
return 'Format invalide.';
}
if (v.badInput) return 'Valeur invalide.';
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);
// Pour l'accessibilité : aria-invalid si non valide
input.setAttribute('aria-invalid', String(!isValid));
};
const validateField = (input) => {
// téléphone facultatif : s'il est vide, considérez-le comme valide
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 });
};
// Validation en direct : au blur et à l'input
form.addEventListener('blur', (e) => {
const target = e.target;
if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement) {
validateField(target);
}
}, true);
// Simple debounce pour ne pas valider agressivement à chaque touche
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 = 'Veuillez vérifier les champs mis en évidence.';
focusFirstInvalid();
return;
}
// Ici, vous enverriez les données au serveur (fetch/AJAX) ou laisseriez le formulaire faire
// Exemple de placeholder :
statusEl.textContent = 'Envoi en cours...';
try {
// Simulation d'envoi
await new Promise((r) => setTimeout(r, 400));
statusEl.textContent = 'Message envoyé avec succès.';
form.reset();
// Nettoyez les états visuels après la réinitialisation
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 = 'Erreur lors de l’envoi. Veuillez réessayer.';
}
});
})();
Pourquoi utiliser la Constraint Validation API
Beaucoup réinventent la roue avec des regex partout et des contrôles manuels. En réalité, les navigateurs modernes offrent un système robuste déjà prêt :
checkValidity()vérifie la validité du champ sans afficher de pop-up du navigateurreportValidity()peut afficher les messages natifs (utile dans des cas simples)validityexpose l’état détaillé (valueMissing, tooShort, typeMismatch, patternMismatch…)
Ainsi, vous vous concentrez sur l’UX (messages, mise en évidence, accessibilité), sans dupliquer des logiques inutiles.
Validation « douce » : quand afficher l’erreur
Une erreur affichée trop tôt peut être gênante. Lignes directrices pratiques :
- affichez les erreurs à la soumission (toujours)
- affichez les erreurs au blur (lorsque l’utilisateur quitte le champ)
- validez « en direct » pendant la saisie uniquement avec un debounce et seulement si le champ a déjà été touché (approche plus avancée)
Dans l’exemple, à la soumission, nous validons tout ; pendant le remplissage, nous validons au blur et avec un léger debounce.
Accessibilité : ce n’est pas facultatif
Deux attentions font la différence :
- liez le texte d’erreur au champ avec
aria-describedby - mettez à jour l’état avec
aria-invalidsi nécessaire
De plus, l’utilisation de aria-live="polite" sur le texte d’erreur permet aux lecteurs d’écran de communiquer le changement.
FAQ rapides
Dois-je utiliser novalidate dans le formulaire ?
Si vous souhaitez gérer les messages et l’interface utilisateur, oui : novalidate désactive les pop-ups natives du navigateur et vous permet une UX cohérente. Si la validation HTML5 standard vous suffit, vous pouvez aussi ne pas l’utiliser.
La regex du téléphone est-elle obligatoire ?
Non. Les numéros de téléphone sont complexes (formats différents). Mieux vaut une validation permissive côté client et une normalisation côté serveur, ou des bibliothèques dédiées lorsque une grande précision est nécessaire.
Puis-je envoyer avec fetch sans recharger la page ?
Oui. Dans le bloc de soumission, vous trouverez déjà un endroit où insérer fetch(). N’oubliez pas : validation serveur toujours, gestion des erreurs et messages clairs côté client.
Conclusion
Un formulaire avec validation côté client en JavaScript ne doit pas être compliqué : HTML5 vous donne déjà des règles solides, JavaScript vous permet de les rendre plus claires, cohérentes et accessibles. Si vous construisez des messages utiles, mettez en évidence les erreurs de la bonne manière et n’oubliez pas l’accessibilité, vous obtiendrez des formulaires plus rapides, moins de frustration et une meilleure qualité de données.
Pubblicato in HTML & CSS, JavaScript
Soyez le premier à commenter