ARIA: роли, состояния, свойства
ARIA добавляет смысл туда, где обычного HTML не хватает, — но злоупотребление ею делает страницу хуже, чем простая семантика.
ARIA (Accessible Rich Internet Applications) — набор HTML-атрибутов, которые сообщают скринридеру роль, состояние и свойства элемента, когда сама разметка их не выражает.
Первое правило ARIA: не используй ARIA
Звучит парадоксально, но это официальная рекомендация спецификации. Полностью она звучит так: «Если можешь использовать нативный HTML-элемент с нужной семантикой и поведением — используй его вместо того, чтобы переопределять элемент через ARIA».
Причина проста: нативные элементы уже несут роль и поведение, а ARIA только описывает, ничего не добавляя к функциональности. Поставив role="button" на <div>, вы пообещали скринридеру кнопку, но фокус и обработку клавиш всё равно придётся писать руками — иначе получится «кнопка», которая не нажимается.
<!-- Лишняя ARIA: тег и так несёт роль navigation -->
<nav role="navigation">...</nav>
<!-- Правильно: роль уже встроена в тег -->
<nav>...</nav>
Запомните: ARIA — это «пластырь» для случаев, когда нативного элемента нет (сложный виджет — вкладки, дерево, слайдер). Для обычной разметки она не нужна и чаще вредит.
Роли: role
Атрибут role сообщает, чем элемент является, перекрывая его исходную роль. Применять его осмысленно стоит лишь там, где готового тега нет:
<!-- Кастомный виджет вкладок: нативного тега нет, роли уместны -->
<div role="tablist">
<button role="tab" aria-selected="true">Описание</button>
<button role="tab" aria-selected="false">Отзывы</button>
</div>
Есть особая безобидная роль role="presentation" (или role="none") — она, наоборот, снимает семантику. Пригодится, если таблица используется не для данных, а для раскладки (хотя так лучше не делать).
Имя элемента: aria-label, aria-labelledby, aria-describedby
Чтобы скринридер прочитал элемент, у того должно быть имя. Обычно имя берётся из текста внутри. Но что, если внутри только иконка?
aria-label задаёт имя прямо строкой. Идеально для кнопок-иконок:
<!-- Внутри только крестик-иконка, текста нет — даём имя через aria-label -->
<button aria-label="Закрыть окно">
<svg>...</svg>
</button>
aria-labelledby берёт имя из текста другого элемента по его id. Удобно, когда подпись уже есть на странице и дублировать её строкой не хочется:
<h2 id="dialog-title">Удалить аккаунт?</h2>
<div role="dialog" aria-labelledby="dialog-title">
...
</div>
aria-describedby добавляет к имени дополнительное описание (его скринридер читает после имени, как подсказку). Часто используется для текста ошибки или поясняющей подписи под полем:
<input id="pass" type="password" aria-describedby="pass-hint">
<p id="pass-hint">Минимум 8 символов</p>
Разница: labelledby — это что это за поле (имя), describedby — дополнительная подсказка к нему.
Состояния и свойства
ARIA умеет описывать не только «что это», но и «в каком оно состоянии». Эти атрибуты вы обязаны менять из JavaScript, когда состояние реально меняется.
aria-expanded — раскрыто или свёрнуто
Для аккордеонов, выпадающих меню, «спойлеров». Кнопка сообщает, раскрыт ли управляемый ею блок:
<button aria-expanded="false" aria-controls="menu">Меню</button>
<ul id="menu" hidden>
<li>...</li>
</ul>
При клике скрипт переключает aria-expanded на "true" и показывает список. Скринридер тогда объявит: «Меню, кнопка, раскрыто».
aria-hidden — спрятать от скринридера
aria-hidden="true" убирает элемент из дерева доступности, оставляя его видимым на экране. Полезно для чисто декоративных иконок рядом с текстом:
<a href="/download">
<svg aria-hidden="true">...</svg> Скачать
</a>
Иконка дублирует слово «Скачать», поэтому её незачем озвучивать дважды. Осторожно: никогда не ставьте aria-hidden="true" на фокусируемый элемент (ссылку, кнопку, поле) — получится фантом: с клавиатуры на него попасть можно, а скринридер про него молчит.
Live-регионы: озвучивание динамики
Когда контент меняется без перезагрузки (появилось сообщение «Сохранено», счётчик корзины, ошибка валидации), зрячий это видит, а пользователь скринридера — нет: его курсор в другом месте страницы. Решают это live-регионы.
Атрибут aria-live говорит: «следи за этим контейнером, и когда его содержимое изменится — прочитай вслух».
<!-- polite: дочитает текущую фразу, потом объявит. Для некритичных уведомлений -->
<div aria-live="polite"></div>
<!-- assertive: перебивает немедленно. Только для важного — ошибок, предупреждений -->
<div aria-live="assertive"></div>
Контейнер обычно пустой и присутствует в разметке заранее. Скрипт вставляет в него текст («Файл загружен») — и скринридер его озвучивает. Значение polite — выбор по умолчанию; assertive перебивает пользователя, поэтому его берегут для действительно срочного.
Как это работает под капотом
ARIA-атрибуты не меняют ни внешний вид, ни поведение элемента — они влияют только на дерево доступности. Браузер, формируя это дерево, подмешивает в роль, имя и состояние узла то, что вы указали в role, aria-label, aria-expanded и прочих атрибутах. Скринридер читает уже изменённое дерево. Поэтому role="button" заставит объявить элемент кнопкой, но Enter сам по себе не заработает — клавиатурную логику дерево доступности не добавляет, её пишет разработчик.
Частые ошибки
- ARIA вместо семантики.
<div role="button">там, где напрашивается<button>. Лишняя работа и источник багов. - Дублирующие роли.
<nav role="navigation">,<ul role="list">— тег уже несёт эту роль. - Статичный
aria-expanded. Поставили"false"в HTML и забыли переключать из JS — скринридер всегда говорит «свёрнуто», даже когда открыто. aria-hidden="true"на фокусируемом элементе. Создаёт «фантом»: фокус есть, озвучки нет.aria-liveдобавляют в момент изменения. Регион должен присутствовать в DOM заранее, иначе первое сообщение не озвучится.
Итоги
- Первое правило ARIA — не использовать ARIA, если хватает нативного HTML.
- ARIA только описывает (роль, имя, состояние), но не добавляет поведения — клавиатуру пишут руками.
aria-labelзадаёт имя строкой,aria-labelledby— из другого элемента,aria-describedby— дополнительную подсказку.- Состояния (
aria-expanded,aria-hidden) нужно синхронно менять из JavaScript. - Live-регионы (
aria-live="polite"/"assertive") озвучивают динамические изменения; контейнер готовят заранее.