lenec ru

← все посты

ARIA: когда нужна, когда вреднее не использовать

10K

Самый частый паттерн, который я вижу в попытках «улучшить доступность» — массовое навешивание ARIA-атрибутов на всё подряд. role="button" на ссылке, aria-label="Кнопка" на кнопке с текстом «Сохранить», aria-describedby на пустую div. Все три случая — антипаттерны. ARIA не делает интерфейс доступнее «по умолчанию». Чаще наоборот: неправильно использованная ARIA выдаёт скринридеру неверную информацию, и пользователь дезориентирован.

Эта статья — про то, когда ARIA реально нужна, когда она просто шум, и где её использование делает только хуже. Без копипасты ARIA Authoring Practices, на которые часто ссылаются и из которых половина рекомендаций устарела.

Первое правило ARIA: не использовать ARIA

В спецификации ARIA это сформулировано почти дословно: «Если можешь использовать нативный HTML-элемент с нужной семантикой и поведением вместо ARIA — используй HTML».

Что это значит на практике. В 90% случаев правильный HTML-тег решает задачу полностью. <button> — это уже role="button", ловит Enter и Space, доступен через Tab, имеет состояние disabled. <input type="checkbox"> — это уже role="checkbox" с состоянием checked. <a href> — это role="link".

Если на странице ты видишь <div role="button" tabindex="0" onClick={...}> — почти всегда это ошибка. Замени на <button>.

Когда без ARIA не обойтись:

  • Виджеты, которым нет HTML-эквивалента: tab, dialog, combobox, tree, slider (в некоторых случаях), tooltip.
  • Динамическая информация о состоянии: aria-expanded, aria-pressed, aria-current.
  • Связь между разрозненными элементами: aria-describedby, aria-labelledby, aria-controls.
  • Live-регионы для асинхронных уведомлений: aria-live.
  • Скрытие декоративных элементов: aria-hidden="true".

aria-label: куда не нужно ставить

Самое частое злоупотребление. Сценарий: разработчик хочет «улучшить доступность» и пишет на каждой кнопке aria-label с тем же текстом, что и внутри.

<!-- избыточно: -->
<button aria-label="Сохранить">Сохранить</button>

aria-label заменяет текстовое содержимое для скринридера. То есть скринридер прочитает только «Сохранить», игнорируя текст внутри. Если они совпадают — оверхед. Если не совпадают — ловушка: визуально пользователь видит одно, а слышит другое.

aria-label оправдан только когда текста внутри нет или он неинформативен:

<button aria-label="Закрыть">
  <svg aria-hidden="true">...</svg>
</button>

<button aria-label="Меню">
  <span aria-hidden="true">☰</span>
</button>

aria-labelledby vs aria-label

Разница простая: aria-label — строка-литерал, aria-labelledby — id другого элемента, чьё содержимое будет использовано как ярлык.

<section aria-labelledby="prefs-heading">
  <h2 id="prefs-heading">Настройки уведомлений</h2>
  ...
</section>

Когда выбирать что. Если ярлык уже есть на странице как видимый текст — aria-labelledby. Дублировать в строку не надо. Если визуального ярлыка нет (иконочная кнопка) — aria-label. Никогда не используй оба одновременно: aria-labelledby побеждает aria-label, и второй просто игнорируется.

Состояния: aria-expanded, aria-pressed, aria-current

Это ARIA, ради которой стоит её знать. Без этих атрибутов скринридер не понимает, в каком состоянии находится элемент.

aria-expanded — для раскрывающихся блоков (аккордеон, dropdown, навигационное меню):

<button
  aria-expanded="false"
  aria-controls="submenu-1"
>
  Раздел
</button>
<ul id="submenu-1" hidden>
  <li>...</li>
</ul>

Когда раздел открывается — меняй на aria-expanded="true". Скринридер озвучит «развёрнуто» / «свёрнуто». Без этого пользователь не знает, что нажатие на кнопку что-то меняет.

aria-pressed — для toggle-кнопок (вкл/выкл, like/unlike). Не для radio-группы.

<button aria-pressed="false" onClick={toggle}>
  Курсив
</button>

aria-current — для текущей страницы в навигации, текущего шага в визарде:

<a href="/about" aria-current="page">О нас</a>

Допустимые значения: page, step, location, date, time, true. Скринридер озвучит «текущая страница».

aria-hidden: точечный инструмент

Скрывает элемент от скринридера, но оставляет видимым визуально. Антипод display: none, который убирает элемент отовсюду.

Корректные применения:

  • Декоративные иконки внутри кнопки: <span aria-hidden="true">→</span>
  • Дублирующий счётчик в карточке, если число и так озвучивается через aria-label.
  • Элементы за модалкой: когда модалка открыта, обернуть всё остальное в <div inert aria-hidden="true">.

Что не делать. Не ставь aria-hidden="true" на интерактивный элемент. Если у тебя <button aria-hidden="true">, кнопка пропадёт из скринридера, но через Tab её всё ещё можно сфокусировать. Пользователь окажется на «невидимой» кнопке. Это баг доступности.

Если хочешь убрать что-то из tab-порядка — используй tabindex="-1" или атрибут inert. inert убирает и из скринридера, и из Tab — это правильный способ погасить блок.

aria-live: уведомления и асинхронность

Когда контент на странице меняется без явного действия пользователя (пришло уведомление, обновился счётчик, появилась ошибка валидации после отправки формы), скринридер по умолчанию это пропускает. Чтобы озвучить изменение, нужен live-регион.

<div aria-live="polite" aria-atomic="true" id="status"></div>

Когда в этот div добавляется текст, скринридер его озвучит. Полностью или частично — зависит от aria-atomic.

  • aria-live="polite" — озвучит, когда пользователь закончит текущее действие. Для большинства уведомлений.
  • aria-live="assertive" — прервёт текущую речь и озвучит немедленно. Для критических ошибок. Использовать редко.
  • aria-atomic="true" — озвучить весь контент региона целиком. Без этого озвучатся только изменившиеся узлы, что часто звучит странно.

Типовая ошибка — заводить новый live-регион на каждое уведомление. Не работает: скринридер должен заранее «слышать» регион. Делай один глобальный регион в layout, и пиши в него:

function announce(message: string) {
  const region = document.getElementById("a11y-status");
  if (!region) return;
  region.textContent = "";
  // даём браузеру время осознать пустое состояние
  requestAnimationFrame(() => {
    region.textContent = message;
  });
}

Этот requestAnimationFrame — ключевой. Если не очистить регион перед записью, при двух одинаковых сообщениях подряд скринридер не озвучит второе (текст не изменился).

Где ARIA делает хуже

Несколько примеров из реальных аудитов.

role="button" на ссылке

<a href="#" role="button">Открыть модалку</a>

Тут разработчик хотел сделать «кнопку», но почему-то остался тег a. Скринридер сообщает «кнопка», а на нажатие Enter элемент ведёт себя как ссылка (срабатывает href). Поведение и роль расходятся.

aria-label на нативных контролах

<input type="submit" value="Отправить" aria-label="Кнопка отправки">

Скринридер озвучит «Кнопка отправки кнопка». Лишнее. Нативный value — уже текстовое имя кнопки.

role="presentation" на интерактивном

<button role="presentation">Закрыть</button>

Кнопка перестаёт быть кнопкой для скринридера. Но всё ещё фокусируется. Получаем элемент без роли, на котором никак не понять «что это вообще».

aria-required на инпуте без HTML required

<input type="email" aria-required="true">

Сообщает скринридеру «обязательное», но браузерная валидация не сработает. Пользователь, не видящий звёздочки, услышит «обязательное», а форма отправится с пустым значением. Используй HTML required, он уже даёт нужную семантику.

Как проверять

Я хожу по странице с открытым accessibility tree в Chrome DevTools (вкладка Elements → подвкладка Accessibility). Для каждого ARIA-атрибута на странице задаю вопрос:

  1. Что он добавляет, чего нет у нативного HTML?
  2. Что услышит скринридер?
  3. Совпадает ли роль ARIA с реальным поведением элемента?

Если на первый вопрос ответ «ничего» — атрибут лишний, удалить. Если на третий «нет» — атрибут вреден, переделать.

Из автоматики: axe-core ловит большинство неправильных применений ARIA. Подключается как Playwright-плагин или через расширение axe DevTools. Не идеален, но 80% типовых ошибок видит.

Что унести с собой

ARIA — точечный инструмент. Хороший фронтенд использует её там, где нативного HTML не хватает: для кастомных виджетов, для динамических состояний, для асинхронных уведомлений. Плохой фронтенд использует её, чтобы прикрыть отсутствие семантики или продублировать то, что и так есть.

Если в твоём проекте на странице сотни ARIA-атрибутов — это повод посмотреть, не подменяет ли она HTML, который изначально надо было использовать. div role="button" — это маркер плохого кода, а не «улучшение доступности».

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.