HTML/CSSMiddleTechnical

Что такое ARIA-атрибуты и почему они важны для доступности (accessibility)?

ARIA-атрибуты расширяют дерево доступности браузера ролями, состояниями и свойствами — вспомогательные технологии (screen readers) читают именно его. Первое правило ARIA: используй нативный HTML-элемент там, где он есть.

Что такое дерево доступности

Браузер строит два дерева из HTML: DOM-дерево (для рендеринга) и дерево доступности (Accessibility Tree) — для вспомогательных технологий: скринридеров, программ управления голосом, брайлевских дисплеев. Каждый узел дерева доступности содержит роль, имя, состояние и свойства. ARIA (Accessible Rich Internet Applications) — это набор атрибутов, которые позволяют управлять этим деревом напрямую, когда нативной семантики HTML недостаточно.

Три группы ARIA-атрибутов

  • Роли (role) — что это за элемент: role="dialog", role="tab", role="alert". Перекрывают нативную семантику тега.
  • Состояния — динамически меняются в зависимости от UI: aria-expanded, aria-checked, aria-disabled, aria-pressed.
  • Свойства — описывают связи и метаданные: aria-label, aria-labelledby, aria-describedby, aria-controls, aria-live.

Первое правило ARIA

Если существует нативный HTML-элемент или атрибут с нужной семантикой — используй его, а не ARIA. <button> уже несёт роль button, фокусируется с клавиатуры и реагирует на Enter/Space. ARIA это не заменит — она только объявляет роль скринридеру, но не добавляет клавиатурное поведение автоматически.

Практический пример: аккордеон

<!-- Кнопка-триггер -->
<button
  id="faq-trigger-1"
  aria-expanded="false"
  aria-controls="faq-panel-1"
>
  Как оформить возврат?
</button>

<!-- Панель -->
<div
  id="faq-panel-1"
  role="region"
  aria-labelledby="faq-trigger-1"
  hidden
>
  <p>Заполните форму на сайте в течение 14 дней...</p>
</div>
const trigger = document.getElementById('faq-trigger-1');
const panel   = document.getElementById('faq-panel-1');

trigger.addEventListener('click', () => {
  const expanded = trigger.getAttribute('aria-expanded') === 'true';
  trigger.setAttribute('aria-expanded', String(!expanded));
  panel.hidden = expanded; // toggle видимость
});

Скринридер объявит: «Как оформить возврат? Кнопка, свёрнуто». После клика — «развёрнуто». Без aria-expanded пользователь не узнает, что панель открылась.

Live-регионы: динамический контент

<!-- Скринридер озвучит содержимое автоматически при изменении -->
<div
  role="status"
  aria-live="polite"
  aria-atomic="true"
></div>
async function submitForm(data) {
  const status = document.querySelector('[role="status"]');
  try {
    await api.post('/orders', data);
    status.textContent = 'Заказ оформлен. Номер: #4821';
  } catch {
    status.textContent = 'Ошибка при оформлении. Попробуйте снова.';
  }
}

aria-live="polite" — скринридер дочитает текущее предложение и только потом огласит обновление. aria-live="assertive" — перебивает немедленно (только для критических ошибок).

Именование элементов: aria-label vs aria-labelledby

<!-- Иконочная кнопка без видимого текста -->
<button aria-label="Закрыть диалог">
  <svg aria-hidden="true">...</svg>
</button>

<!-- Поле, связанное с видимым заголовком -->
<h2 id="shipping-title">Адрес доставки</h2>
<input aria-labelledby="shipping-title" type="text" />

aria-hidden="true" на SVG убирает его из дерева доступности — иначе скринридер попытается прочитать пути SVG как текст.

Подводные камни

  • ARIA не добавляет поведение. role="button" на <div> объявит роль скринридеру, но div не фокусируется Tab и не реагирует на Enter без явного tabindex="0" и обработчика keydown. Используй <button>.
  • Несинхронное состояние. Открыл панель, но забыл переключить aria-expanded — скринридер говорит «свёрнуто», хотя контент виден. Состояния ARIA нужно обновлять в том же обработчике, что и визуальные изменения.
  • Дублирование имён. Кнопка с текстом «Удалить» и aria-label="Удалить товар из корзины» — нормально. Кнопка с текстом «Удалить товар из корзины» и тем же aria-label — скринридер прочитает label, проигнорирует текст. Следи, чтобы видимый текст и aria-имя не противоречили друг другу.
  • Избыточный aria-hidden. aria-hidden="true" на контейнере, внутри которого есть фокусируемые элементы — создаёт «невидимые» кнопки, на которые попадает Tab-фокус, но скринридер не объявляет их содержимое.
  • role="presentation" не то же самое, что aria-hidden. role="presentation" убирает семантику элемента, но дочерние элементы остаются в дереве. Это не «скрыть» элемент.
  • Живые регионы — только пустые при инициализации. Если aria-live-контейнер уже содержит текст при загрузке страницы, скринридеры не объявят его — только последующие изменения.
  • Поддержка комбинаций browser+AT неоднородна. aria-expanded на <select> игнорируется в большинстве скринридеров. Перед внедрением нестандартного паттерна проверяй матрицу поддержки на a11ysupport.io.
  • Не злоупотребляй role="alert". Alert — assertive live region. Несколько одновременных alert-элементов на странице (ошибки валидации формы) создадут хаос — скринридер будет перебивать себя. Используй одну область с aria-live="polite" или суммарное сообщение.

Common mistakes

  • Смешивать «ARIA-атрибуты» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «ARIA-атрибуты» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет доступность интерактивных интерфейсов, когда нативной семантики недостаточно.
  • Показывает на примере, как работает: ARIA дополняет дерево доступности ролями, состояниями и свойствами, но не добавляет клавиатурное поведение и не исправляет неверный выбор HTML-элемента автоматически.
  • Называет production-нюанс и граничный случай для темы «ARIA-атрибуты».

Sources

Related topics