Le problème
Marquer les champs obligatoires avec un état d'erreur dès le chargement de la page peut être déroutant. Idéalement, un champ obligatoire ne doit être considéré comme "non valide" que si l'utilisateur a tenté de le remplir et a échoué.
La solution
La pseudo-classe :user-invalid résout parfaitement ce problème. Pour un champ obligatoire, il ne correspondra pas au chargement de page. La correspondance n'est établie que si :
- L'utilisateur interagit avec le champ (par exemple, il saisit un caractère et le supprime), puis le quitte (flou), le laissant vide.
- L'utilisateur tente d'envoyer le formulaire alors que le champ est vide.
Stratégie d'implémentation
- Contrainte HTML : ajoutez l'attribut
requiredà vos entrées. - Commentaires visuels : utilisez
:user-invalidpour appliquer un style de bordure rouge et afficher un texte d'aide "Obligatoire". - Timing : fiez-vous au timing natif du navigateur pour le feedback visuel. Vous n'avez plus besoin de gestionnaires
onBlurpour ajouter une classetouched, même si un peu de JavaScript est toujours nécessaire pour synchroniser les attributs ARIA (voir ci-dessous).
Guide d'implémentation
1. Structure HTML
<form id="feedback-form">
<div class="field">
<label for="full-name">Full Name</label>
<input
type="text"
id="full-name"
name="full-name"
required
aria-errormessage="name-error"
>
<!-- MANDATORY: Include an icon or distinct non-color indicator alongside error text -->
<div id="name-error" class="error-msg">
<span aria-hidden="true">❌</span> This field is required.
</div>
</div>
</form>
2. CSS
.error-msg {
display: none;
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/*
Only highlight empty required fields AFTER the user visits them.
MANDATORY: Provide multiple indicators (border shift + helper text/icon) to avoid color-only state communication.
*/
input:user-invalid {
border-color: #d93025;
background-color: #fce8e6;
}
input:user-invalid + .error-msg {
display: block;
}
/* Optional: Subtle indicator for required fields that are valid */
input:required:user-valid {
border-color: #188038;
border-width: 2px;
}
3. Synchronisation de l'état JavaScript
OBLIGATOIRE : Étant donné que :user-invalid est un état visuel, vous DEVEZ fournir un pont JavaScript pour synchroniser aria-invalid="true" de manière dynamique pour les technologies d'assistance lorsqu'un utilisateur quitte un champ non valide ou tente d'envoyer un formulaire.
const form = document.getElementById('feedback-form');
const syncAriaInvalid = (input) => {
if (!input.checkValidity()) {
input.setAttribute('aria-invalid', 'true');
} else {
input.removeAttribute('aria-invalid');
}
};
// Sync on blur when a user finishes interacting
form.addEventListener('blur', (e) => {
if (e.target.matches('input[required]')) {
syncAriaInvalid(e.target);
}
}, true);
// Sync all required fields when submission is attempted
form.addEventListener('submit', () => {
form.querySelectorAll('input[required]').forEach(syncAriaInvalid);
});
// Remove error state immediately upon correction
form.addEventListener('input', (e) => {
if (e.target.matches('input[required]') && e.target.checkValidity()) {
e.target.removeAttribute('aria-invalid');
}
});
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
- Astérisques : il est toujours recommandé d'indiquer visuellement les champs obligatoires (par exemple, avec un astérisque
*) dans le libellé, afin que les utilisateurs sachent à quoi s'attendre avant d'interagir. - Boutons d'envoi : contrairement aux boutons
disabled, laissez votre bouton d'envoi activé. Si l'utilisateur clique dessus, le navigateur déclenche automatiquement:user-invalidsur tous les champs obligatoires vides et sélectionne le premier. C'est excellent pour l'accessibilité et l'expérience utilisateur. - Accessibilité : 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);
});