Problem
Wyświetlanie błędów weryfikacji w momencie, gdy użytkownik skupia się na polu i zaczyna pisać, jest przedwczesne i rozprasza uwagę. Na przykład gdy użytkownik wpisuje adres e-mail (np. „uzytkownik@gm”) lub hasło o złożonych wymaganiach, pole jest technicznie nieprawidłowe, dopóki nie zostanie wypełnione. Standardowy styl :invalid powoduje natychmiastowe wyświetlenie stanu błędu, co może być frustrujące dla użytkownika.
Rozwiązanie
Pseudoklasa :user-invalid pozwala odroczyć stan błędu do momentu, gdy użytkownik „zatwierdzi” wartość (przez usunięcie zaznaczenia pola) lub spróbuje przesłać formularz. Dzięki temu opinie dotyczące weryfikacji są przekazywane dopiero po zakończeniu interakcji użytkownika z polem.
Strategia wdrażania
- Ograniczenie HTML: używaj standardowych atrybutów HTML5, takich jak
type="email",patternirequired, aby aktywować wbudowaną logikę sprawdzania poprawności w przeglądarce. - Informacje wizualne: używaj
:user-invalid, aby zastosować styl błędu dopiero po interakcji. - Pozytywne wzmocnienie: możesz użyć
:user-valid, aby wyświetlić zielony wskaźnik „sukcesu” po spełnieniu wymagań. - Łagodne przywracanie: gdy tylko użytkownik poprawi dane wejściowe na prawidłowy format,
:user-invalidprzestaje dopasowywać, natychmiast usuwając stan błędu.
Przewodnik po implementacji
Przypadek użycia 1. Sprawdzanie poprawności adresu e-mail
OBOWIĄZKOWO: w polach adresu e-mail używaj standardowych atrybutów HTML5. Komunikat o błędzie jest domyślnie ukryty i wyświetla się tylko wtedy, gdy przeglądarka stwierdzi, że użytkownik opuścił pole, które jest w nieprawidłowym stanie.
<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;
}
Przypadek użycia 2. Złożoność hasła
OBOWIĄZKOWE: zdefiniuj regułę złożoności, używając wzorca Regex Lookahead w atrybucie pattern. Lista reguł jest wyświetlana nad polem wprowadzania, aby pomóc użytkownikowi, i jest wyróżniona, jeśli wystąpi błąd.
<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;
}
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ę
- Ułatwienia dostępu:
- OBOWIĄZKOWO: użyj
aria-describedby, aby połączyć listę reguł z danymi wejściowymi. - NIE: ukrywaj całkowicie list reguł, dopóki dane wejściowe nie będą prawidłowe. Użytkownicy muszą wiedzieć, co mają wpisać.
- OBOWIĄZKOWO: użyj
- Limity atrybutu Pattern: OBOWIĄZKOWE: atrybut
patternwykonuje pełne dopasowanie (domyślne^...$). Upewnij się, że wyrażenie regularne hasła uwzględnia cały ciąg znaków. - Rygorystyczność sprawdzania poprawności: pamiętaj, że domyślne sprawdzanie poprawności
type="email"w przeglądarce jest dość liberalne (np.user@localservermoże zostać zaakceptowane). Jeśli potrzebujesz bardziej rygorystycznej walidacji, możesz użyć bardziej rozbudowanej biblioteki walidacji lub walidacji niestandardowej wraz ztype="email". - Zarządzanie fokusem: OBOWIĄZKOWE: jeśli użytkownik prześle formularz z nieprawidłowym polem, przeglądarka automatycznie ustawi fokus na pierwszym nieprawidłowym polu. Twoje style
:user-invalidzostaną zastosowane natychmiast, ponieważ próba przesłania jest traktowana jako interakcja. - Spójne działanie ARIA: 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);
});