Le problème
Afficher des erreurs de validation dès qu'un utilisateur sélectionne un champ et commence à taper est prématuré et distrayant. Par exemple, lorsqu'un utilisateur saisit une adresse e-mail (par exemple, "user@gm") ou un mot de passe avec des exigences complexes, le champ est techniquement non valide jusqu'à ce qu'il soit complété. Le style :invalid standard entraîne l'affichage immédiat d'un état d'erreur, ce qui frustre l'utilisateur.
La solution
La pseudo-classe :user-invalid vous permet de différer l'état d'erreur jusqu'à ce que l'utilisateur ait "validé" une valeur (en floutant le champ) ou tenté d'envoyer le formulaire. Cela garantit que les commentaires de validation ne sont fournis qu'une fois que l'utilisateur a fini d'interagir avec le champ.
Stratégie d'implémentation
- Contrainte HTML : utilisez des attributs HTML5 standards tels que
type="email",patternetrequiredpour déclencher la logique de validation intégrée du navigateur. - Commentaires visuels : utilisez
:user-invalidpour appliquer un style d'erreur uniquement après l'interaction. - Renforcement positif : vous pouvez utiliser
:user-validpour afficher un indicateur de réussite vert une fois que les conditions sont remplies. - Récupération fluide : dès que l'utilisateur corrige l'entrée pour qu'elle soit au bon format,
:user-invalidcesse de correspondre, ce qui supprime immédiatement l'état d'erreur.
Guide d'implémentation
Cas d'utilisation 1 : Validation des adresses e-mail
OBLIGATOIRE : Utilisez des attributs HTML5 standards pour les champs d'adresse e-mail. Le message d'erreur est masqué par défaut et ne s'affiche que lorsque le navigateur détermine que l'utilisateur a quitté le champ dans un état non valide.
<form>
<div class="field">
<label for="email">Email Address</label>
<!-- MANDATORY: Place format hints above the input so autocomplete popovers don't cover them during editing -->
<span id="email-hint" class="hint">Format: you@example.com</span>
<!-- DO: Use standard HTML validation attributes like type="email" and required -->
<input
type="email"
id="email"
name="email"
required
autocomplete="email"
aria-describedby="email-hint"
aria-errormessage="email-error"
>
<div id="email-error" class="error-msg">
<span aria-hidden="true">❌</span> Please enter a valid email address.
</div>
</div>
</form>
.hint {
display: block;
color: #5f6368;
font-size: 0.85rem;
margin-bottom: 0.25rem;
}
.error-msg {
display: none;
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/*
DO: Only show error styles after user interaction.
Use multiple indicators (border/background shift + icon/text) to avoid color-only states.
*/
input:user-invalid {
border-color: #d93025;
background-color: #fce8e6;
}
/* DO: Reveal the error message using the adjacent sibling selector */
input:user-invalid + .error-msg {
display: block;
}
/* DO: Provide a clear success indication on :user-valid */
input:user-valid {
border-color: #188038;
}
Cas d'utilisation 2 : Complexité des mots de passe
OBLIGATOIRE : Définissez la règle de complexité à l'aide d'un modèle d'assertion simple (lookahead) d'expression régulière dans l'attribut pattern. La liste des règles s'affiche au-dessus du champ de saisie pour guider l'utilisateur. Elle est mise en surbrillance en cas d'erreur.
<form>
<div class="field">
<label for="password">New Password</label>
<!-- MANDATORY: Place hints and rules above the input so mobile keyboards do not obscure them -->
<ul id="password-rules" class="rules-list">
<li>At least 8 characters</li>
<li>One uppercase letter</li>
<li>One number</li>
<li>One special character</li>
</ul>
<!-- DO: Use pattern and minlength for complex password validation
DO: Match all constraints with lookaheads via pattern attribute
-->
<input
type="password"
id="password"
autocomplete="new-password"
required
pattern="(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[\W_]).{8,}"
minlength="8"
aria-describedby="password-rules"
>
</div>
</form>
/* DO: State the default styling as neutral */
.rules-list {
color: #5f6368;
margin-bottom: 0.5rem;
}
/* DO: Show invalid state (After interaction): Error */
input:user-invalid {
border-color: #d93025;
background-color: #fce8e6;
}
/* DO: Highlight rules list when error is shown using the modern :has() selector */
.field:has(input:user-invalid) .rules-list {
color: #d93025;
font-weight: 600;
}
/* DO: Add success indications for :user-valid state */
input:user-valid {
border-color: #188038;
}
/* DO: Hide rules once satisfied */
.field:has(input:user-valid) .rules-list {
display: none;
}
Création de remplacement et compatibilité avec les navigateurs
Solutions de secours et compatibilité des navigateurs pour :user-valid et :user-invalid
CSS pour la création de remplacement
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
input:user-invalid + .error-msg,
input.user-invalid-fallback + .error-msg {
display: block;
}
JavaScript de remplacement
Utilisez un utilitaire réutilisable qui suit l'état d'interaction à l'aide d'un WeakMap. Cela évite de polluer le DOM avec des classes ou des attributs de données "sales".
const UserInvalidFallback = (() => {
const dirtyState = new WeakMap();
const updateState = (input) => {
const isValid = input.checkValidity();
// Update both visual and ARIA state
input.classList.toggle('user-invalid-fallback', !isValid);
input.classList.toggle('user-valid-fallback', isValid);
if (!isValid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
const handleEvent = (event) => {
const input = event.target;
if (event.type === 'reset') {
const controls = input.elements || [];
for (const control of controls) {
dirtyState.delete(control);
control.classList.remove('user-invalid-fallback');
control.classList.remove('user-valid-fallback');
control.removeAttribute('aria-invalid');
}
return;
}
if (!input.checkValidity) return;
if (event.type === 'input' || event.type === 'change') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasInteracted = true;
dirtyState.set(input, state);
if (state.hasBlurred) {
updateState(input);
}
} else if (event.type === 'blur') {
const state = dirtyState.get(input) || { hasInteracted: false, hasBlurred: false };
state.hasBlurred = true;
dirtyState.set(input, state);
if (state.hasInteracted) {
updateState(input);
}
}
};
const init = (root = document) => {
if (CSS.supports('selector(:user-invalid)')) return;
root.addEventListener('blur', handleEvent, true); // Capture phase
root.addEventListener('input', handleEvent);
root.addEventListener('change', handleEvent);
root.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize for a specific form
const form = document.querySelector('#demo-form');
UserInvalidFallback.init(form);
Autres points à prendre en compte
- Accessibilité :
- OBLIGATOIRE : Utilisez
aria-describedbypour associer la liste de règles à l'entrée. - À NE PAS FAIRE : Masquer complètement les listes de règles tant que la saisie n'est pas valide. Les utilisateurs doivent savoir quoi saisir.
- OBLIGATOIRE : Utilisez
- Limites de l'attribut de modèle : OBLIGATOIRE : l'attribut
patterneffectue une correspondance complète (^...$implicite). Assurez-vous que l'expression régulière de votre mot de passe tient compte de l'intégralité de la chaîne. - Rigueur de la validation : notez que la validation
type="email"par défaut du navigateur est assez permissive (par exemple,user@localserverpeut être accepté). Si vous avez besoin d'une validation plus stricte, vous devrez peut-être utiliser une bibliothèque de validation plus robuste ou une fonction de validation personnalisée avectype="email". - Gestion du curseur : OBLIGATOIRE : si un utilisateur envoie le formulaire avec un champ non valide, le navigateur place automatiquement le curseur sur le premier champ non valide. Vos styles
:user-invalids'appliqueront immédiatement, car une tentative d'envoi est considérée comme une interaction. - Expérience ARIA cohérente : l'élément
:user-invalidnatif ne se synchronise pas automatiquement avec les attributs ARIA. Ajoutez le code JavaScript suivant pour quearia-invalidreste synchronisé avec l'état visuel :
// Sync aria-invalid with the CSS :user-invalid state
const syncAria = (el) => {
el.setAttribute?.('aria-invalid', el.matches(':user-invalid') ? 'true' : 'false');
};
// Update on blur (to show error) and input (to clear it)
document.addEventListener('blur', (e) => syncAria(e.target), true);
document.addEventListener('input', (e) => {
if (e.target.hasAttribute('aria-invalid')) syncAria(e.target);
});