Библиотека Slim Select заменяет стандартный HTML-элемент
<select> собственной DOM-структурой. После такой
замены часть встроенной семантики браузера теряется, поэтому корректная
настройка ARIA-атрибутов становится обязательной частью разработки
доступного интерфейса.
ARIA (Accessible Rich Internet Applications) обеспечивает передачу информации вспомогательным технологиям: экранным дикторам, системам голосового управления, навигации с клавиатуры и другим инструментам доступности.
Без правильно настроенных ARIA-атрибутов пользователь может столкнуться со следующими проблемами:
<select> и проблема кастомизацииОбычный HTML-элемент:
<select>
<option>JavaScript</option>
<option>TypeScript</option>
</select>
уже содержит встроенную семантику:
После инициализации Slim Select структура становится значительно сложнее:
<div class="ss-main">
<div class="ss-single-selected">JavaScript</div>
<div class="ss-content">
<div class="ss-option">JavaScript</div>
<div class="ss-option">TypeScript</div>
</div>
</div>
Обычные <div> не имеют семантики. Для браузера и
screen reader это просто контейнеры. Именно поэтому требуется ручное
назначение ролей и ARIA-атрибутов.
Наиболее важные атрибуты:
| Атрибут | Назначение |
|---|---|
aria-expanded |
состояние раскрытия списка |
aria-selected |
выбран ли элемент |
aria-disabled |
отключён ли элемент |
aria-labelledby |
связь с заголовком |
aria-label |
текстовое описание |
aria-controls |
связь управляющего элемента со списком |
aria-activedescendant |
активный элемент |
aria-multiselectable |
поддержка множественного выбора |
aria-hidden |
скрытие элементов от screen reader |
aria-live |
озвучивание динамических изменений |
aria-expandedАтрибут сообщает, раскрыт ли выпадающий список.
<div
class="ss-main"
aria-expanded="false">
</div>
После открытия:
<div
class="ss-main"
aria-expanded="true">
</div>
Screen reader может озвучивать:
Список свернут
или:
Список раскрыт
Без этого пользователь не понимает текущее состояние интерфейса.
При ручной кастомизации Slim Select атрибут необходимо синхронизировать с состоянием компонента.
const trigger = document.querySelector('.ss-main')
trigger.setAttribute('aria-expanded', 'true')
При закрытии:
trigger.setAttribute('aria-expanded', 'false')
aria-selectedПоказывает, выбран ли конкретный элемент списка.
<div
class="ss-option"
aria-selected="true">
JavaScript
</div>
Для остальных элементов:
<div
class="ss-option"
aria-selected="false">
TypeScript
</div>
При перемещении по списку диктор может озвучивать:
JavaScript, выбран
или:
TypeScript, не выбран
При изменении выбора необходимо обновлять атрибут:
option.setAttribute('aria-selected', 'true')
С остальных элементов атрибут также должен обновляться:
options.forEach(item => {
item.setAttribute('aria-selected', 'false')
})
aria-disabledARIA сообщает, что элемент недоступен для взаимодействия.
<div
class="ss-option"
aria-disabled="true">
Недоступный пункт
</div>
disabled и aria-disableddisabledПолностью блокирует элемент на уровне браузера.
<select disabled>
aria-disabledТолько сообщает состояние доступности.
<div aria-disabled="true">
Элемент всё ещё может получать события, если дополнительно не отключить обработчики.
option.setAttribute('aria-disabled', 'true')
option.classList.add('disabled')
aria-labelЕсли у поля отсутствует <label>, требуется
добавить текстовое описание вручную.
<div
class="ss-main"
aria-label="Выбор языка программирования">
</div>
Подходит для:
Без aria-label screen reader может озвучивать:
Кнопка
вместо:
Выбор языка программирования
aria-labelledbyНаиболее предпочтительный способ описания поля.
<label id="skills-label">
Навыки
</label>
<div
class="ss-main"
aria-labelledby="skills-label">
</div>
aria-labelaria-labelledby:
Допускается объединение нескольких идентификаторов.
<div
aria-labelledby="title description">
</div>
aria-controlsУказывает, какой элемент контролируется текущим компонентом.
<div
class="ss-main"
aria-controls="skills-list">
</div>
<div id="skills-list">
</div>
Screen reader понимает:
aria-activedescendantПри перемещении стрелками необходимо указывать текущий активный пункт.
<div
class="ss-content"
aria-activedescendant="option-js">
</div>
<div id="option-js">
JavaScript
</div>
При нажатии стрелки вниз:
list.setAttribute(
'aria-activedescendant',
'option-ts'
)
Screen reader получает информацию:
TypeScript
при перемещении по списку.
Без атрибута навигация становится «немой».
aria-multiselectableДля multi-select необходимо указать возможность выбора нескольких элементов.
<div
role="listbox"
aria-multiselectable="true">
</div>
Диктор может сообщить:
Список с множественным выбором
aria-selectedКаждый элемент должен иметь собственное состояние:
<div aria-selected="true">
JavaScript
</div>
<div aria-selected="false">
Vue
</div>
aria-hiddenНекоторые части интерфейса не должны озвучиваться.
Пример:
<span
class="icon"
aria-hidden="true">
▼
</span>
Подходит для:
Нельзя скрывать:
aria-liveИспользуется для уведомлений о смене состояния.
<div aria-live="polite"></div>
status.innerText =
'Выбрано 3 элемента'
Screen reader автоматически озвучит изменение.
| Значение | Поведение |
|---|---|
polite |
мягкое уведомление |
assertive |
немедленное озвучивание |
Атрибуты работают совместно с ролями.
role="combobox"Главный контрол выбора.
<div
role="combobox"
aria-expanded="false">
</div>
role="listbox"Контейнер списка.
<div role="listbox">
</div>
role="option"Элемент списка.
<div
role="option"
aria-selected="true">
JavaScript
</div>
<label id="framework-label">
Фреймворк
</label>
<div
class="ss-main"
role="combobox"
aria-expanded="false"
aria-labelledby="framework-label"
aria-controls="framework-list">
React
</div>
<div
id="framework-list"
role="listbox">
<div
id="react-option"
role="option"
aria-selected="true">
React
</div>
<div
id="vue-option"
role="option"
aria-selected="false">
Vue
</div>
</div>
new SlimSelect({
select: '#skills'
})
const main =
document.querySelector('.ss-main')
main.setAttribute(
'aria-label',
'Выбор навыков'
)
main.addEventListener('click', () => {
const expanded =
main.getAttribute('aria-expanded')
main.setAttribute(
'aria-expanded',
expanded === 'true'
? 'false'
: 'true'
)
})
Slim Select поддерживает встроенный поиск.
Поле поиска также требует ARIA-настройки.
<input
type="search"
aria-label="Поиск навыков">
<input
aria-controls="search-results">
Плохо:
<div class="ss-option">
Правильно:
<div
role="option">
aria-expandedОшибка:
aria-expanded="false"
при открытом списке.
aria-selectedScreen reader не понимает текущее состояние выбора.
aria-hiddenОшибка:
<button aria-hidden="true">
Такой элемент становится невидимым для систем доступности.
Необходимо контролировать:
Наиболее распространённые инструменты:
| Screen Reader | Платформа |
|---|---|
| NVDA | Windows |
| JAWS | Windows |
| VoiceOver | macOS |
| TalkBack | Android |
Инструмент Chrome DevTools автоматически проверяет:
aria-controlsconst listId =
'ss-list-' + Date.now()
list.id = listId
main.setAttribute(
'aria-controls',
listId
)
aria-selectedfunction updateSelected(option) {
document
.querySelectorAll('.ss-option')
.forEach(item => {
item.setAttribute(
'aria-selected',
'false'
)
})
option.setAttribute(
'aria-selected',
'true'
)
}
aria-live<div
id="status"
aria-live="polite">
</div>
status.textContent =
'Выбрано: React'
Большинство современных браузеров поддерживают:
Однако поведение screen reader может различаться.
Например:
combobox.<select> при возможностиЕсли сложная кастомизация не требуется, стандартный
<select> остаётся наиболее доступным решением.
Чрезмерная вложенность:
<div>
<div>
<div>
<div>
усложняет навигацию вспомогательным технологиям.
ARIA-атрибуты должны изменяться одновременно с визуальным состоянием интерфейса.
Компонент считается недоступным, если:
При открытии списка фокус должен переходить в активную область.
searchInput.focus()
Нельзя полностью отключать outline:
Плохо:
*:focus {
outline: none;
}
Лучше:
.ss-option:focus {
outline: 2px solid #3b82f6;
}