ARIA - набор атрибутов, сообщающих вспомогательным технологиям роль, состояние и отношения элемента. Ключевое слово - сообщают: ARIA ничего не рисует и не добавляет поведения, лишь меняет, как элемент выглядит в accessibility API - дереве доступности для скринридеров. Новичок реагирует естественно: сыплет атрибутами - role, aria-label, aria-hidden где что-то мешает. Кажется, чем больше ARIA, тем доступнее. На практике наоборот.
Первое правило ARIA: если нужную семантику и поведение уже даёт нативный HTML, используйте его, а не переопределяйте чужой тег ролью. За правилом - горькая статистика: в обследовании миллиона страниц WebAIM у страниц с ARIA в среднем больше ошибок доступности, чем без неё. Отсюда поговорка: отсутствие ARIA лучше плохой ARIA. Причина проста: нативная button даёт фокусируемость, обработку Enter и Space, роль button и корректное состояние - бесплатно и везде. Заменив её на div с role=button, весь багаж воспроизводишь руками.
Здесь и кроется главная ловушка. Заявленная роль - не украшение, а контракт. Объявив role=dialog, вы обязаны запереть фокус в окне, вернуть его на триггер при закрытии и обработать Escape; объявив role=tab - взять стрелочную навигацию и aria-selected. ARIA описывает, чем элемент притворяется, а выполнять обещанное - работа кода. Скринридер объявит div с role=button кнопкой, но Enter не сработает: обработчика нет. Это хуже честного неозвученного div - интерфейс солгал.
У ARIA три вида атрибутов, различать их полезно. Роль задаёт тип объекта - диалог, вкладка, статус. Состояние описывает изменяемое положение - раскрыт ли список, выбран ли пункт. Свойство фиксирует отношение - какой элемент управляется кнопкой, какой заголовок её называет.
| Механизм | Что описывает | Пример |
|---|---|---|
| Role | Тип объекта | dialog, tab, status |
| State | Изменяемое состояние | aria-expanded, aria-selected |
| Property | Отношение или характеристика | aria-controls, aria-labelledby |
Возьмём страницу товара в мультиязычном магазине. Кнопка Фильтры сворачивает и разворачивает панель. ARIA здесь уместна: нативной раскрывающейся секции с нужной семантикой нет. Кнопка остаётся нативным button - клавиатура и роль даром, - а два атрибута выражают недостающее: aria-expanded сообщает состояние, aria-controls связывает кнопку с панелью. При нажатии код переключает hidden и меняет aria-expanded на true. Атрибут и реальное состояние обязаны совпадать, иначе скринридер объявит ложь.
<button
aria-expanded="false"
aria-controls="filters"
>Фильтры</button>
<section id="filters" hidden>…</section>Имя, которое скринридер произносит, браузер вычисляет строгим алгоритмом - accessible name computation. Приоритеты такие: aria-labelledby перекрывает aria-label, тот - нативную подпись вроде label или содержимого, и лишь в конце идёт title. Отсюда правило: видимый текст почти всегда предпочтительнее aria-label. Видимую подпись читают все, она не расходится с интерфейсом при переводе и не устаревает молча. aria-label оправдан, где видимого текста нет: у иконки-кнопки без подписи. Подменять им внятный текст - значит плодить две версии правды.
Отдельный механизм - живые области. Атрибут aria-live заставляет скринридер объявлять изменения в блоке, не уводя туда фокус: polite ждёт паузы и годится для статусов вроде Товар добавлен в корзину, assertive перебивает и приберегается для ошибок. Типичный провал: команда прячет декоративную иконку корзины через aria-hidden, но тем же махом скрывает счётчик внутри - и незрячий перестаёт слышать, сколько в корзине. ARIA сработала точно по написанному; неверным было написанное. Отсюда дисциплина: сначала нативный HTML, ARIA - только когда платформа не выражает нужное, и каждый атрибут подкреплён кодом, держащим обещание.