Проблема
Для обязательных раскрывающихся списков (например, "Выберите страну") стандартная проверка сразу помечает поле как недействительное, если вариант по умолчанию имеет пустое значение. Это может создать визуальный шум. Мы хотим показывать ошибку, только если пользователь открывает меню и закрывает его, не выбрав вариант, или пытается отправить форму.
Решение
Псевдокласс :user-invalid отлично работает с элементами <select>. Оно учитывает действия пользователя: простая загрузка страницы или фокусировка/размытие без внесения изменений не считаются взаимодействием, поэтому поле остается нейтральным, пока пользователь не попытается сделать выбор.
Стратегия реализации
- Ограничение HTML. Используйте тег
<select>с атрибутомrequired. Первый вариант должен иметь значениеvalue=""и в идеале быть отключен или скрыт, чтобы пользователь выбрал действительный вариант. - Визуальная обратная связь. Используйте
:user-invalid, чтобы настроить стиль границы поля выбора. - Время. Браузер считает, что пользователь взаимодействовал с полем, если он изменил значение (даже если вернул его в исходное недопустимое состояние) до того, как убрал фокус с элемента управления, или при отправке формы.
Руководство по внедрению
1. Структура HTML
Здесь важен параметр placeholder.
<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;
}
Резервные объявления и поддержка браузеров
Резервные варианты и поддержка браузеров для :user-valid и :user-invalid
Резервный ССЦ
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
Используйте многоразовую утилиту, которая отслеживает состояние взаимодействия с помощью WeakMap. Это позволяет избежать загрязнения DOM "грязными" классами или атрибутами данных.
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);
О чем ещё нужно помнить
- Поведение на мобильных устройствах. На мобильных устройствах событие blur может происходить по-разному в зависимости от того, какой инструмент выбора даты используется в операционной системе. Рекомендуем тестировать на реальных устройствах.
- Специальные возможности. Нативный элемент
:user-invalidне синхронизируется автоматически с атрибутами ARIA. Добавьте следующий код JavaScript, чтобы синхронизироватьaria-invalidс визуальным состоянием:
// 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);
});