Validation instantanée des formulaires
Formulaire d’inscription avec état en temps réel, messages d’erreur et retour de soumission.
Après activation de JavaScript, ce cas peut être modifié, exécuté et enregistré. Code source fixe ci-dessous :
<!doctype html>
<html lang="fr" dir="ltr">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Validation instantanée des formulaires</title>
<style>
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; color: #23252b; font-family: "Avenir Next", "Microsoft YaHei", sans-serif; background: #f3e7da; }
.shell { width: min(920px, 100%); display: grid; grid-template-columns: .86fr 1.14fr; overflow: hidden; border: 1px solid #d6c7b8; border-radius: 28px; background: #fffdf9; box-shadow: 0 32px 80px rgba(73,52,34,.14); }
.aside { position: relative; min-height: 620px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; padding: clamp(28px, 5vw, 48px); color: #fff; background: #28483f; }
.aside::before { content: ""; position: absolute; width: 330px; height: 330px; top: 110px; left: -120px; border: 70px solid rgba(246,180,100,.22); border-radius: 50%; }
.logo, .quote { position: relative; z-index: 1; }
.logo { font: 800 18px/1 Georgia, serif; }
.logo i { color: #f6b464; font-style: normal; }
.quote strong { display: block; font: 500 clamp(30px, 5vw, 46px)/1.1 Georgia, "STSong", serif; }
.quote p { color: #b9c9c4; line-height: 1.7; }
.form-wrap { padding: clamp(28px, 6vw, 58px); }
.eyebrow { color: #c76343; font-size: 11px; font-weight: 900; letter-spacing: .18em; }
h1 { margin: 9px 0 8px; font: 700 clamp(34px, 6vw, 52px)/1 Georgia, "STSong", serif; }
.intro { margin: 0 0 30px; color: #7b7d82; font-size: 14px; }
.field { margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; color: #4a4c52; font-size: 12px; font-weight: 800; }
input[type="text"], input[type="email"], input[type="password"] { width: 100%; padding: 13px 14px; border: 1px solid #d8d7d2; border-radius: 11px; color: #24262b; font: inherit; background: #fff; transition: border-color .18s, box-shadow .18s; }
input:focus { border-color: #3c7566; outline: 0; box-shadow: 0 0 0 3px rgba(60,117,102,.12); }
.field.is-invalid input { border-color: #c44f3f; box-shadow: 0 0 0 3px rgba(196,79,63,.1); }
.field.is-valid input { border-color: #3c7566; }
.error { min-height: 17px; margin: 5px 2px 0; color: #c44f3f; font-size: 11px; }
.password-meta { display: flex; gap: 5px; margin-top: 8px; }
.bar { flex: 1; height: 3px; border-radius: 3px; background: #e4e1db; }
.bar.active { background: #3c7566; }
.terms { display: flex; align-items: flex-start; gap: 9px; margin: 4px 0 22px; color: #6c6e73; font-size: 12px; line-height: 1.5; }
.terms input { width: 17px; height: 17px; margin: 1px 0 0; accent-color: #3c7566; }
.submit { width: 100%; padding: 14px 18px; border: 0; border-radius: 11px; color: #fff; font: inherit; font-weight: 850; background: #c76343; box-shadow: 0 7px 0 #8e402c; cursor: pointer; }
.submit:active { transform: translateY(4px); box-shadow: 0 3px 0 #8e402c; }
.submit:focus-visible { outline: 3px solid #28483f; outline-offset: 3px; }
.result { min-height: 20px; margin: 18px 0 0; color: #28705b; font-size: 13px; font-weight: 800; text-align: center; }
@media (max-width: 720px) { .shell { grid-template-columns: 1fr; } .aside { min-height: 240px; } .quote p { display: none; } }
@media (prefers-reduced-motion: reduce) { input { transition: none !important; } }
</style>
</head>
<body>
<main class="shell">
<aside class="aside"><div class="logo">Field<i>Note</i></div><div class="quote"><strong>Organiser les idées en un plan atteignable.</strong><p>Un espace calme pour noter, organiser et partager votre prochaine étape.</p></div></aside>
<section class="form-wrap"><span class="eyebrow">CREATE ACCOUNT</span><h1>Créer un nouveau compte</h1><p class="intro">Toutes les vérifications s’effectuent sur la page actuelle et aucune donnée ne sera envoyée.</p>
<form id="signup" novalidate>
<div class="field"><label for="name">Surnom</label><input id="name" name="name" type="text" autocomplete="name" maxlength="24" aria-describedby="nameError"><p id="nameError" class="error"></p></div>
<div class="field"><label for="email">Adresse e-mail</label><input id="email" name="email" type="email" autocomplete="email" aria-describedby="emailError"><p id="emailError" class="error"></p></div>
<div class="field"><label for="password">Mot de passe</label><input id="password" name="password" type="password" autocomplete="new-password" aria-describedby="passwordError"><div class="password-meta" aria-hidden="true"><i class="bar"></i><i class="bar"></i><i class="bar"></i></div><p id="passwordError" class="error"></p></div>
<label class="terms"><input id="terms" type="checkbox"><span>J’ai lu et j’accepte les conditions de l’exemple, et je comprends qu’il s’agit uniquement d’un formulaire de démonstration local.</span></label>
<button class="submit" type="submit">Inscription terminée</button><p id="result" class="result" role="status" aria-live="polite"></p>
</form>
</section>
</main>
<script>
const form = document.querySelector('#signup');
const fields = {
name: { input: document.querySelector('#name'), check: value => value.trim().length >= 2 ? '' : 'Le pseudonyme doit comporter au moins 2 caractères.' },
email: { input: document.querySelector('#email'), check: value => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? '' : 'Veuillez saisir une adresse e-mail valide.' },
password: { input: document.querySelector('#password'), check: value => value.length >= 8 && /[A-Za-z]/.test(value) && /\d/.test(value) ? '' : 'Le mot de passe doit comporter au moins 8 caractères et contenir à la fois des lettres et des chiffres.' }
};
function validateField(key) {
const field = fields[key];
const message = field.check(field.input.value);
const wrapper = field.input.closest('.field');
wrapper.classList.toggle('is-invalid', Boolean(message));
wrapper.classList.toggle('is-valid', !message);
field.input.setAttribute('aria-invalid', String(Boolean(message)));
document.querySelector(`#${key}Error`).textContent = message;
return !message;
}
Object.entries(fields).forEach(([key, field]) => {
field.input.addEventListener('blur', () => validateField(key));
field.input.addEventListener('input', () => { if (field.input.closest('.field').classList.contains('is-invalid')) validateField(key); if (key === 'password') updateStrength(); });
});
function updateStrength() {
const value = fields.password.input.value;
const score = [value.length >= 8, /[A-Za-z]/.test(value) && /\d/.test(value), /[^A-Za-z0-9]/.test(value)].filter(Boolean).length;
document.querySelectorAll('.bar').forEach((bar, index) => bar.classList.toggle('active', index < score));
}
form.addEventListener('submit', event => {
event.preventDefault();
const valid = Object.keys(fields).map(validateField).every(Boolean);
const result = document.querySelector('#result');
if (!document.querySelector('#terms').checked) { result.textContent = 'Veuillez d’abord cocher pour accepter les conditions de l’exemple.'; return; }
result.textContent = valid ? 'Vérification réussie ! Cet exemple ne créera pas réellement de compte.' : 'Veuillez vérifier les champs signalés en rouge.';
if (!valid) document.querySelector('[aria-invalid="true"]')?.focus();
});
</script>
</body>
</html>