Le problème
La validation HTML5 standard fournit un retour visuel (via :invalid ou :user-invalid), mais elle ne se synchronise pas automatiquement avec les attributs d'accessibilité tels que aria-invalid.
Si vous utilisez le style :invalid standard, les lecteurs d'écran peuvent annoncer "Entrée non valide" dès qu'un utilisateur accède à un champ obligatoire actuellement vide. Cela crée une expérience perturbante pour les utilisateurs de technologies d'assistance, car l'erreur est annoncée avant que l'interaction n'ait eu lieu.
La solution
Nous voulons que l'état programmatic (aria-invalid="true") soit appliqué exactement au même moment que l'état visual (:user-invalid). Étant donné que :user-invalid s'appuie sur l'indicateur interne "user-interacted" du navigateur, nous pouvons utiliser JavaScript pour vérifier que ce sélecteur correspond lors des événements d'interaction standards.
Pour en savoir plus, consultez MDN aria-invalid.
Stratégie d'implémentation
- Couche visuelle : utilisez la propriété CSS
:user-invalidpour afficher les bordures/icônes. - Couche d'accessibilité : utilisez
aria-invalidetaria-errormessagepour communiquer l'état à la technologie d'assistance. - Couche visuelle et d'accessibilité de transition : créez un utilitaire JavaScript léger qui écoute les événements
bluretinput, vérifie si l'élément correspond à:user-invalidet met à jour les attributs ARIA en conséquence.
Guide d'implémentation
1. Structure HTML
Associez votre entrée à son message d'erreur à l'aide de aria-errormessage (ou aria-describedby pour une compatibilité plus étendue).
<form>
<div class="field">
<label for="email">Email</label>
<input
type="email"
id="email"
required
aria-errormessage="email-error"
>
<span id="email-error" class="error-msg">
Please enter a valid email address.
</span>
</div>
</form>
2. CSS
Contrôlez la visibilité du message d'erreur à l'aide de la pseudo-classe native :user-invalid.
.error-msg {
display: none;
color: #d93025;
}
/* Show error message when input is user-invalid */
input:user-invalid ~ .error-msg {
display: block;
}
/* Optional: Visual cues on the input itself */
input:user-invalid {
border-color: #d93025;
}
3. JavaScript
Comme il n'existe pas d'événement "UserInvalidChanged", utilisez les événements de formulaire standards pour vérifier l'état.
const updateAriaState = (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
// Check if the browser currently considers this input "user-invalid"
const isUserInvalid = input.matches(':user-invalid');
if (isUserInvalid) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Listen on the document to handle dynamically added fields.
// 'blur' and 'focus' do not bubble, so we must use the capture phase (true).
document.addEventListener('blur', updateAriaState, true);
document.addEventListener('focus', updateAriaState, true);
// Also update on input if we've already shown the error,
// so the error clears immediately when fixed.
document.addEventListener('input', (event) => {
const input = event.target;
if (!input.matches?.('input, textarea, select')) return;
const hasAriaInvalid = input.hasAttribute('aria-invalid');
const ariaInvalid = input.getAttribute('aria-invalid');
if (hasAriaInvalid && ariaInvalid === 'true') {
updateAriaState(event);
}
});
Création de remplacement et compatibilité avec les navigateurs
Détection de caractéristiques
Vous pouvez vérifier la compatibilité en CSS et en JavaScript.
Vérification JavaScript :
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS pour la création de remplacement
Pour vous assurer que votre logique de remplacement est visuellement indiscernable du comportement natif, vous devez appliquer vos styles d'erreur à la fois à la pseudo-classe et à votre classe de remplacement.
/* Apply error styles to both native selector and fallback class */
input:user-invalid,
input.user-invalid-fallback {
border-color: #d93025;
background-color: #fce8e6;
}
/* Show error message for both cases */
input:user-invalid ~ .error-msg,
input.user-invalid-fallback ~ .error-msg {
display: block;
}
Logique de remplacement
Si :user-invalid est manquant, suivez manuellement l'état d'interaction à l'aide d'un WeakMap.
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' && input.matches?.('form')) {
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.matches?.('input, textarea, select')) 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 = () => {
if (CSS.supports('selector(:user-invalid)')) return;
document.addEventListener('blur', handleEvent, true); // Capture phase required
document.addEventListener('input', handleEvent, true);
document.addEventListener('change', handleEvent, true);
document.addEventListener('reset', handleEvent, true); // Capture resets
};
return { init };
})();
// Initialize globally
UserInvalidFallback.init();
Autres points à prendre en compte
aria-livevsaria-errormessage:aria-errormessageassocie l'entrée au texte, mais les lecteurs d'écran peuvent ne pas l'annoncer immédiatement lorsqu'il apparaît (uniquement lorsque l'entrée est sélectionnée).- Si vous avez besoin d'une annonce immédiate lorsque l'erreur s'affiche (par exemple, en cas de flou), envisagez d'ajouter
role="alert"ouaria-live="polite"au conteneur du message d'erreur, mais effectuez des tests approfondis pour éviter une "double annonce" lorsque l'utilisateur sélectionne le champ pour corriger l'erreur.
Internationalisation :
- Assurez-vous que le contenu textuel de votre message d'erreur (
#email-error) est traduit. La logique reste la même.
- Assurez-vous que le contenu textuel de votre message d'erreur (