Il problema
La convalida HTML5 standard fornisce un feedback visivo (tramite :invalid o :user-invalid), ma non si sincronizza automaticamente con gli attributi di accessibilità come aria-invalid.
Se utilizzi lo stile :invalid standard, gli screen reader potrebbero annunciare "Voce non valida" nel momento in cui un utente passa a un campo obbligatorio attualmente vuoto. Ciò crea un'esperienza di disturbo per gli utenti che utilizzano tecnologie per la disabilità, poiché l'errore viene annunciato prima che si verifichi l'interazione.
La soluzione
Vogliamo che lo stato programmatico (aria-invalid="true") venga applicato nello stesso momento in cui viene applicato lo stato visivo (:user-invalid). Poiché :user-invalid si basa sul flag interno "user-interacted" del browser, possiamo utilizzare JavaScript per verificare che questo selettore corrisponda durante gli eventi di interazione standard.
Per maggiori dettagli, consulta la pagina MDN aria-invalid.
Strategia di implementazione
- Livello visivo: utilizza il CSS
:user-invalidper mostrare bordi/icone. - Livello di accessibilità: utilizza
aria-invalidearia-errormessageper comunicare lo stato alle tecnologie per la disabilità. - Collega il livello visivo e quello di accessibilità: crea un'utilità JavaScript leggera che ascolta gli eventi
blureinput, controlla se l'elemento corrisponde a:user-invalide aggiorna gli attributi ARIA di conseguenza.
Guida all'implementazione
1. Struttura HTML
Collega l'input al messaggio di errore utilizzando aria-errormessage (o aria-describedby per un supporto più ampio).
<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
Controlla la visibilità del messaggio di errore utilizzando la pseudoclasse nativa :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
Poiché non esiste un evento "UserInvalidChanged", esegui l'hook agli eventi del modulo standard per controllare lo stato.
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);
}
});
Fallback e supporto del browser
Rilevamento delle caratteristiche
Puoi verificare il supporto in CSS e JavaScript.
Controllo JavaScript:
if (!CSS.supports('selector(:user-invalid)')) {
// Fallback logic here
}
CSS per il fallback
Per assicurarti che la logica di riserva sia visivamente indistinguibile dal comportamento nativo, devi applicare gli stili di errore sia alla pseudo-classe sia alla classe di riserva.
/* 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;
}
Logica di riserva
Se :user-invalid non è presente, monitora manualmente lo stato dell'interazione utilizzando 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();
Altre considerazioni
aria-livevs.aria-errormessage:aria-errormessagecollega l'input al testo, ma gli screen reader potrebbero non annunciarlo immediatamente quando viene visualizzato (solo quando l'input viene selezionato).- Se hai bisogno di un annuncio immediato quando viene visualizzato l'errore (ad esempio, in caso di sfocatura), valuta la possibilità di aggiungere
role="alert"oaria-live="polite"al contenitore del messaggio di errore, ma esegui un test approfondito per evitare un "doppio annuncio" quando l'utente attiva il campo per correggerlo.
Internazionalizzazione:
- Assicurati che i contenuti testuali del messaggio di errore (
#email-error) siano tradotti. La logica rimane la stessa.
- Assicurati che i contenuti testuali del messaggio di errore (