Problem
Oznaczanie wymaganych pól jako błędnych natychmiast po wczytaniu strony może być mylące. Najlepiej, aby pole wymagane wyglądało na „nieprawidłowe” tylko wtedy, gdy użytkownik próbował je wypełnić i nie udało mu się to.
Rozwiązanie
Pseudoklasa :user-invalid idealnie rozwiązuje ten problem. W przypadku pola wymaganego nie będzie ono dopasowywane podczas wczytywania strony. Dopasowanie nastąpi tylko wtedy, gdy:
- Użytkownik wchodzi w interakcję z polem (np. wpisuje znak i go usuwa), a następnie opuszcza je (blur), pozostawiając puste.
- Użytkownik próbuje przesłać formularz, gdy pole jest puste.
Strategia wdrażania
- Ograniczenie HTML: dodaj atrybut
requireddo danych wejściowych. - Wizualne informacje zwrotne: użyj
:user-invalid, aby nadać obramowaniu czerwony kolor i wyświetlić tekst pomocniczy „Wymagane”. - Czas: polegaj na natywnym czasie przeglądarki, aby uzyskać informacje wizualne. Nie musisz już używać modułów obsługi
onBlur, aby dodać klasętouched, ale nadal potrzebujesz kodu JavaScript do synchronizowania atrybutów ARIA (patrz poniżej).
Przewodnik po implementacji
1. Struktura 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. Synchronizacja stanu JavaScriptu
OBOWIĄZKOWE: ponieważ :user-invalid to stan wizualny, MUSISZ udostępnić most JavaScript, aby dynamicznie synchronizować aria-invalid="true" na potrzeby technologii wspomagających osoby z niepełnosprawnością, gdy użytkownik opuści nieprawidłowe pole lub spróbuje przesłać formularz.
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');
}
});
Kreacje zastępcze i obsługa przeglądarek
Zastępcze i obsługiwane przez przeglądarkę dla :user-valid i :user-invalid
CSS dla kreacji zastępczej
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;
}
Zastępczy tag JavaScript
Użyj narzędzia wielokrotnego użytku, które śledzi stan interakcji za pomocą WeakMap. Dzięki temu unikniesz zaśmiecania modelu DOM „brudnymi” klasami lub atrybutami danych.
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);
Inne rzeczy, które warto wziąć pod uwagę
- Gwiazdki: nadal sprawdzoną metodą jest wizualne oznaczanie wymaganych pól (np. gwiazdką
*) w etykiecie, aby użytkownicy wiedzieli, czego się spodziewać, zanim wejdą w interakcję. - Przyciski przesyłania: w przeciwieństwie do przycisków
disabledprzycisk przesyłania powinien być włączony. Jeśli użytkownik go kliknie, przeglądarka automatycznie uruchomi:user-invalidwe wszystkich pustych wymaganych polach i ustawi kursor w pierwszym z nich. To świetne rozwiązanie pod względem ułatwień dostępu i wrażeń użytkownika. - Ułatwienia dostępu: natywny element
:user-invalidnie synchronizuje się automatycznie z atrybutami ARIA. Dodaj ten kod JavaScript, aby zachować synchronizacjęaria-invalidze stanem wizualnym:
// 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);
});