Скринридеры — программы экранного доступа, преобразующие содержимое интерфейса в голосовой вывод или текст для устройств Брайля. При работе с HTML-формами они анализируют:
Компоненты выбора значений относятся к наиболее сложным элементам для
скринридеров, поскольку часто реализуются не на основе нативного
<select>, а через кастомные DOM-структуры.
Slim Select создаёт собственный интерфейс поверх оригинального
<select>, сохраняя часть нативного поведения. Это
упрощает поддержку accessibility по сравнению с полностью кастомными
реализациями.
При инициализации библиотека:
<select>;Пример:
<select id="country">
<option value="kz">Казахстан</option>
<option value="ru">Россия</option>
<option value="uz">Узбекистан</option>
</select>
new SlimSelect({
select: '#country'
})
После инициализации в DOM появляются:
<div class="ss-main">
<div class="ss-single-selected">Казахстан</div>
</div>
Исходный <select> обычно получает скрывающие
стили:
display: none;
Именно здесь возникает главный риск accessibility: некоторые
скринридеры игнорируют элементы с display: none.
Если оригинальный <select> полностью удаляется из
accessibility tree, скринридер может:
Неправильный подход:
select {
display: none;
}
Более безопасный способ визуального скрытия:
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip: rect(0 0 0 0);
border: 0;
}
Тогда элемент остаётся доступным скринридеру.
Скринридеры опираются на связь <label> и элемента
формы.
Правильная структура:
<label for="city">
Город
</label>
<select id="city">
<option>Алматы</option>
<option>Астана</option>
</select>
Slim Select сохраняет эту связь через оригинальный
<select>.
Ошибочная реализация:
<div>Город</div>
<select id="city">
В этом случае скринридер может озвучить поле как:
combo box
без имени поля.
Если визуальный label отсутствует, необходимо использовать
aria-label.
Пример:
<select id="theme" aria-label="Выбор темы">
<option>Светлая</option>
<option>Тёмная</option>
</select>
Либо:
<select
id="theme"
aria-labelledby="theme-title">
</select>
<span id="theme-title">
Выбор темы
</span>
Скринридер должен сообщать:
Ожидаемый результат:
Город, Алматы, комбинированный список
Если значение не озвучивается, возможны причины:
Скринридеры тесно связаны с клавиатурной навигацией. Если фокус не перемещается корректно, голосовой вывод становится хаотичным.
Slim Select использует собственные focusable-элементы.
Важно проверять:
document.activeElement
Например:
document.addEventListener('focusin', () => {
console.log(document.activeElement)
})
Типичные ошибки:
Элементы интерфейса Slim Select должны быть доступны через Tab.
Корректный вариант:
<div class="ss-main" tabindex="0">
Некорректно:
<div class="ss-main">
Без tabindex скринридер может пропустить элемент.
Опасный вариант:
<div tabindex="-1">
Элемент становится недоступным для обычной навигации.
Для кастомных select-компонентов крайне важны ARIA-роли.
Основные роли:
| Роль | Назначение |
|---|---|
| combobox | поле выбора |
| listbox | контейнер списка |
| option | элемент списка |
| group | группа опций |
Главный интерактивный элемент должен иметь роль:
<div
class="ss-main"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox">
</div>
Скринридер понимает:
Атрибут сообщает, открыт ли список.
Закрытое состояние:
aria-expanded="false"
Открытое состояние:
aria-expanded="true"
При открытии dropdown значение должно обновляться динамически.
Пример:
main.setAttribute('aria-expanded', 'true')
Без этого скринридер не сообщит пользователю о раскрытии списка.
Контейнер опций:
<div role="listbox">
Скринридер начинает воспринимать вложенные элементы как варианты выбора.
Каждый пункт:
<div role="option">
Казахстан
</div>
Для выбранного элемента:
<div
role="option"
aria-selected="true">
Казахстан
</div>
Атрибут показывает текущее состояние опции.
Пример:
aria-selected="true"
или:
aria-selected="false"
Скринридеры озвучивают:
selected
или:
не выбрано
в зависимости от языка системы.
Для multiple-select accessibility становится сложнее.
Пример:
<select multiple id="skills">
<option>JavaScript</option>
<option>TypeScript</option>
<option>Node.js</option>
</select>
Скринридер должен понимать:
Для этого используют:
aria-multiselectable="true"
на контейнере listbox.
Пример:
<div
role="listbox"
aria-multiselectable="true">
</div>
В multi-select часто появляются кнопки удаления tag-элементов.
Плохой пример:
<span class="remove">×</span>
Скринридер озвучит:
times
или вообще ничего.
Корректный вариант:
<button
aria-label="Удалить JavaScript">
×
</button>
При выборе значения полезно уведомлять скринридер о произошедшем изменении.
Пример области уведомлений:
<div
id="announce"
aria-live="polite">
</div>
Обновление:
announce.textContent =
'Выбрано значение Казахстан'
Скринридер озвучит изменение автоматически.
Режимы aria-live:
| Значение | Поведение |
|---|---|
| polite | дождаться паузы |
| assertive | прервать текущую речь |
Для select-компонентов обычно подходит:
aria-live="polite"
Slim Select поддерживает встроенный поиск.
При accessibility важно:
Пример улучшения:
new SlimSelect({
select: '#users',
searchPlaceholder: 'Поиск пользователя'
})
Поле поиска должно иметь:
<input
type="search"
aria-label="Поиск пользователя">
При фильтрации желательно сообщать количество результатов.
Пример:
announce.textContent =
`Найдено ${results.length} элементов`
Это особенно важно при больших списках.
Если данные подгружаются динамически:
fetch('/api/countries')
необходимо сообщать о:
Пример:
<div aria-live="polite" id="status"></div>
status.textContent = 'Загрузка данных'
После завершения:
status.textContent = 'Данные загружены'
Если select недоступен:
<select disabled>
необходимо передавать состояние в кастомный интерфейс.
Пример:
<div
aria-disabled="true">
</div>
Скринридер озвучит:
disabled
Для обязательных полей:
<select required>
или:
aria-required="true"
Скринридер сообщает:
required
Ошибки должны быть связаны с select через
aria-describedby.
Пример:
<select
id="country"
aria-describedby="country-error">
</select>
<div id="country-error">
Выберите страну
</div>
Дополнительно:
aria-invalid="true"
При успешной валидации:
select.setAttribute(
'aria-invalid',
'false'
)
При ошибке:
select.setAttribute(
'aria-invalid',
'true'
)
Наиболее важные комбинации для тестирования:
| Скринридер | Платформа |
|---|---|
| NVDA | Windows |
| JAWS | Windows |
| VoiceOver | macOS / iOS |
| Narrator | Windows |
| TalkBack | Android |
NVDA особенно чувствителен к:
Частая проблема:
blank
вместо значения select.
Причина обычно связана с div-элементом без роли combobox.
VoiceOver активно использует:
Нарушение порядка tabindex часто приводит к пропуску элементов.
Chrome DevTools содержит вкладку Accessibility.
Проверяются:
При перемещении по списку скринридер должен понимать активную опцию.
Пример:
<div
role="combobox"
aria-activedescendant="option-2">
</div>
Активный пункт:
<div
id="option-2"
role="option">
Казахстан
</div>
Для корректной работы со скринридерами необходимы:
| Клавиша | Действие |
|---|---|
| Tab | переход |
| Enter | выбор |
| Space | открытие |
| ArrowUp | вверх |
| ArrowDown | вниз |
| Escape | закрытие |
Отсутствие стандартного поведения ломает accessibility.
Одна из самых распространённых проблем кастомных select-компонентов — чрезмерное количество div без семантики.
Плохой пример:
<div>
<div>
<div>Казахстан</div>
</div>
</div>
Скринридер не понимает назначение структуры.
Правильный вариант:
<div
role="listbox">
<div role="option">
Казахстан
</div>
</div>
Иконки и декоративные элементы не должны озвучиваться.
Пример:
<span aria-hidden="true">
▼
</span>
Порядок перехода должен быть логичным:
Нарушение tab order вызывает серьёзные проблемы при screen reader navigation.
Наиболее безопасная стратегия:
Полностью кастомный select без ARIA и keyboard support практически всегда становится недоступным для пользователей screen reader.