Поддержка скринридеров

Скринридеры — программы экранного доступа, преобразующие содержимое интерфейса в голосовой вывод или текст для устройств Брайля. При работе с HTML-формами они анализируют:

  • структуру DOM;
  • семантику элементов;
  • ARIA-атрибуты;
  • состояние элементов;
  • связи между label и form-control;
  • доступность клавиатурной навигации.

Компоненты выбора значений относятся к наиболее сложным элементам для скринридеров, поскольку часто реализуются не на основе нативного <select>, а через кастомные DOM-структуры.

Slim Select создаёт собственный интерфейс поверх оригинального <select>, сохраняя часть нативного поведения. Это упрощает поддержку accessibility по сравнению с полностью кастомными реализациями.


Как Slim Select взаимодействует со скринридерами

При инициализации библиотека:

  1. скрывает оригинальный <select>;
  2. создаёт собственную визуальную структуру;
  3. синхронизирует значения между нативным элементом и кастомным интерфейсом;
  4. обрабатывает фокусировку и клавиатурное управление.

Пример:

<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

Если оригинальный <select> полностью удаляется из accessibility tree, скринридер может:

  • не увидеть поле;
  • не распознать label;
  • не озвучить текущее значение;
  • не сообщить о required-состоянии;
  • потерять информацию о списке вариантов.

Неправильный подход:

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> и элемента формы.

Правильная структура:

<label for="city">
  Город
</label>

<select id="city">
  <option>Алматы</option>
  <option>Астана</option>
</select>

Slim Select сохраняет эту связь через оригинальный <select>.

Ошибочная реализация:

<div>Город</div>

<select id="city">

В этом случае скринридер может озвучить поле как:

combo box

без имени поля.


Поддержка aria-label

Если визуальный 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>

Проверка озвучивания текущего значения

Скринридер должен сообщать:

  • название поля;
  • текущее значение;
  • тип элемента;
  • состояние доступности.

Ожидаемый результат:

Город, Алматы, комбинированный список

Если значение не озвучивается, возможны причины:

  • неправильный focus management;
  • отсутствие синхронизации с select;
  • потеря aria-атрибутов;
  • кастомный div вместо семантического элемента.

Управление фокусом

Скринридеры тесно связаны с клавиатурной навигацией. Если фокус не перемещается корректно, голосовой вывод становится хаотичным.

Slim Select использует собственные focusable-элементы.

Важно проверять:

document.activeElement

Например:

document.addEventListener('focusin', () => {
  console.log(document.activeElement)
})

Типичные ошибки:

  • фокус уходит в body;
  • tabindex=“-1” на активном элементе;
  • dropdown не получает фокус;
  • option нельзя выбрать с клавиатуры.

Значение tabindex

Элементы интерфейса Slim Select должны быть доступны через Tab.

Корректный вариант:

<div class="ss-main" tabindex="0">

Некорректно:

<div class="ss-main">

Без tabindex скринридер может пропустить элемент.

Опасный вариант:

<div tabindex="-1">

Элемент становится недоступным для обычной навигации.


Использование ARIA-ролей

Для кастомных select-компонентов крайне важны ARIA-роли.

Основные роли:

Роль Назначение
combobox поле выбора
listbox контейнер списка
option элемент списка
group группа опций

Роль combobox

Главный интерактивный элемент должен иметь роль:

<div
  class="ss-main"
  role="combobox"
  aria-expanded="false"
  aria-haspopup="listbox">
</div>

Скринридер понимает:

  • элемент раскрывается;
  • содержит список;
  • поддерживает выбор значения.

aria-expanded

Атрибут сообщает, открыт ли список.

Закрытое состояние:

aria-expanded="false"

Открытое состояние:

aria-expanded="true"

При открытии dropdown значение должно обновляться динамически.

Пример:

main.setAttribute('aria-expanded', 'true')

Без этого скринридер не сообщит пользователю о раскрытии списка.


Роль listbox

Контейнер опций:

<div role="listbox">

Скринридер начинает воспринимать вложенные элементы как варианты выбора.


Роль option

Каждый пункт:

<div role="option">
  Казахстан
</div>

Для выбранного элемента:

<div
  role="option"
  aria-selected="true">
  Казахстан
</div>

aria-selected

Атрибут показывает текущее состояние опции.

Пример:

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>

aria-live для динамических изменений

При выборе значения полезно уведомлять скринридер о произошедшем изменении.

Пример области уведомлений:

<div
  id="announce"
  aria-live="polite">
</div>

Обновление:

announce.textContent =
  'Выбрано значение Казахстан'

Скринридер озвучит изменение автоматически.


polite и assertive

Режимы aria-live:

Значение Поведение
polite дождаться паузы
assertive прервать текущую речь

Для select-компонентов обычно подходит:

aria-live="polite"

Поддержка поиска внутри Slim Select

Slim Select поддерживает встроенный поиск.

При accessibility важно:

  • наличие текстового поля;
  • корректное имя поля;
  • фокусировка;
  • озвучивание результатов.

Пример улучшения:

new SlimSelect({
  select: '#users',
  searchPlaceholder: 'Поиск пользователя'
})

Поле поиска должно иметь:

<input
  type="search"
  aria-label="Поиск пользователя">

Озвучивание количества найденных элементов

При фильтрации желательно сообщать количество результатов.

Пример:

announce.textContent =
  `Найдено ${results.length} элементов`

Это особенно важно при больших списках.


Accessibility при асинхронной загрузке

Если данные подгружаются динамически:

fetch('/api/countries')

необходимо сообщать о:

  • начале загрузки;
  • завершении;
  • ошибках.

Пример:

<div aria-live="polite" id="status"></div>
status.textContent = 'Загрузка данных'

После завершения:

status.textContent = 'Данные загружены'

Поддержка disabled-состояния

Если select недоступен:

<select disabled>

необходимо передавать состояние в кастомный интерфейс.

Пример:

<div
  aria-disabled="true">
</div>

Скринридер озвучит:

disabled

Required-поля

Для обязательных полей:

<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"

Динамическое обновление aria-invalid

При успешной валидации:

select.setAttribute(
  'aria-invalid',
  'false'
)

При ошибке:

select.setAttribute(
  'aria-invalid',
  'true'
)

Совместимость со скринридерами

Наиболее важные комбинации для тестирования:

Скринридер Платформа
NVDA Windows
JAWS Windows
VoiceOver macOS / iOS
Narrator Windows
TalkBack Android

Особенности NVDA

NVDA особенно чувствителен к:

  • отсутствию role;
  • неправильному aria-expanded;
  • некорректному focus management.

Частая проблема:

blank

вместо значения select.

Причина обычно связана с div-элементом без роли combobox.


Особенности VoiceOver

VoiceOver активно использует:

  • aria-label;
  • aria-labelledby;
  • порядок DOM;
  • клавиатурную навигацию.

Нарушение порядка tabindex часто приводит к пропуску элементов.


Проверка accessibility в DevTools

Chrome DevTools содержит вкладку Accessibility.

Проверяются:

  • role;
  • computed name;
  • keyboard-focusable;
  • accessibility tree;
  • active descendant.

aria-activedescendant

При перемещении по списку скринридер должен понимать активную опцию.

Пример:

<div
  role="combobox"
  aria-activedescendant="option-2">
</div>

Активный пункт:

<div
  id="option-2"
  role="option">
  Казахстан
</div>

Поддержка клавиш навигации

Для корректной работы со скринридерами необходимы:

Клавиша Действие
Tab переход
Enter выбор
Space открытие
ArrowUp вверх
ArrowDown вниз
Escape закрытие

Отсутствие стандартного поведения ломает accessibility.


Избежание div soup

Одна из самых распространённых проблем кастомных select-компонентов — чрезмерное количество div без семантики.

Плохой пример:

<div>
  <div>
    <div>Казахстан</div>
  </div>
</div>

Скринридер не понимает назначение структуры.

Правильный вариант:

<div
  role="listbox">

  <div role="option">
    Казахстан
  </div>

</div>

Скрытие декоративных элементов

Иконки и декоративные элементы не должны озвучиваться.

Пример:

<span aria-hidden="true">
  ▼
</span>

Проверка tab order

Порядок перехода должен быть логичным:

  1. label;
  2. select;
  3. dropdown;
  4. элементы списка;
  5. следующая форма.

Нарушение tab order вызывает серьёзные проблемы при screen reader navigation.


Accessibility-first подход при настройке Slim Select

Наиболее безопасная стратегия:

  • сохранять оригинальный select;
  • не удалять label;
  • использовать ARIA-роли;
  • поддерживать клавиатурную навигацию;
  • синхронизировать состояния;
  • озвучивать динамические изменения;
  • тестировать интерфейс реальными скринридерами;
  • не полагаться только на визуальное тестирование.

Полностью кастомный select без ARIA и keyboard support практически всегда становится недоступным для пользователей screen reader.