Het probleem
Voor verplichte dropdownmenu's (bijvoorbeeld Kies een land) markeert de standaardvalidatie het veld onmiddellijk als ongeldig als de standaardoptie een lege waarde heeft. Dit kan visuele ruis veroorzaken. We willen de fout alleen tonen als de gebruiker het menu opent en sluit zonder een optie te kiezen of het formulier probeert in te dienen.
De oplossing
De pseudoklasse :user-invalid werkt naadloos met <select>-elementen. Het respecteert de interactiestroom van de gebruiker: alleen de pagina laden of de focus verplaatsen zonder een wijziging aan te brengen, telt niet als een interactie. Het veld blijft dus neutraal totdat de gebruiker actief een selectie probeert te maken.
Implementatiestrategie
- HTML-beperking: Gebruik een
<select>metrequired. De eerste optie moetvalue=""hebben en idealiter worden uitgezet/verborgen om een geldige keuze af te dwingen. - Visuele feedback: Gebruik
:user-invalidom de rand van het selectievak op te maken. - Timing: De browser beschouwt het veld als 'interacted' als de gebruiker de waarde wijzigt (zelfs terug naar de standaard ongeldige status) voordat de gebruiker de focus van het bedieningselement haalt of het formulier indient.
Implementatiehandleiding
1. HTML-structuur
De optie 'placeholder' is hierbij essentieel.
<form>
<div class="field">
<label for="country">Country</label>
<select
id="country"
name="country"
required
aria-errormessage="country-error"
>
<option value="" disabled selected>Select a country...</option>
<option value="us">United States</option>
<option value="ca">Canada</option>
<option value="uk">United Kingdom</option>
</select>
<div id="country-error" class="error-msg">
Please select a country.
</div>
</div>
</form>
2. CSS
.error-msg {
display: none;
color: #d93025;
font-size: 0.875rem;
margin-top: 0.25rem;
}
/*
Only show error after the user visits the select menu.
*/
select:user-invalid {
border-color: #d93025;
background-color: #fce8e6;
}
select:user-invalid + .error-msg {
display: block;
}
select:user-valid {
border-color: #188038;
}
Terugval en browserondersteuning
Fallbacks en browserondersteuning voor :user-valid en :user-invalid
CSS voor reserve
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-reserve
Gebruik een herbruikbaar hulpprogramma dat de interactiestatus bijhoudt met een WeakMap. Zo voorkomt u dat het DOM wordt vervuild met 'vuile' klassen of gegevenskenmerken.
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);
Andere overwegingen
- Mobiel gedrag: Op mobiele apparaten kan vervaging anders werken, afhankelijk van de OS-kiezer. We raden je aan te testen op echte apparaten.
- Toegankelijkheid: Systeemeigen
:user-invalidwordt niet automatisch gesynchroniseerd met ARIA-kenmerken. Voeg de volgende JavaScript toe omaria-invalidgesynchroniseerd te houden met de visuele status:
// 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);
});