ARIA атрибуты

ARIA (Accessible Rich Internet Applications) -- набор HTML-атрибутов, дополняющих семантику элементов для вспомогательных технологий (screen readers, switch devices).

Зачем нужно

Когда нативной HTML-семантики недостаточно (кастомные компоненты, динамический контент), ARIA заполняет пробелы. Она сообщает screen reader-у роль элемента, его состояние и связи с другими элементами.

Первое правило ARIA: не используй ARIA, если можно использовать нативный HTML-элемент. <button> лучше, чем <div role="button">.

Где используется

  • Кастомные UI-компоненты (табы, аккордеоны, модалки)
  • Динамический контент (уведомления, загрузка)
  • Когда нативной семантики недостаточно
  • SPA-приложения с динамическим обновлением DOM

Предпосылки

Правила ARIA

  1. Не используй ARIA, если есть нативный HTML-элемент
  2. Не меняй нативную семантику (не ставь role="heading" на <div> если можно <h2>)
  3. Все интерактивные ARIA-элементы должны быть доступны с клавиатуры
  4. Не используй role="presentation" или aria-hidden="true" на фокусируемых элементах
  5. Все интерактивные элементы должны иметь доступное имя

role -- роль элемента

<!-- Landmark roles (лучше использовать нативные элементы) -->
<div role="banner">         <!-- вместо <header> -->
<div role="navigation">     <!-- вместо <nav> -->
<div role="main">           <!-- вместо <main> -->
<div role="contentinfo">    <!-- вместо <footer> -->
<div role="complementary">  <!-- вместо <aside> -->
<div role="search">         <!-- вместо <search> -->

<!-- Widget roles -->
<div role="button" tabindex="0">Кнопка</div>     <!-- лучше <button> -->
<div role="alert">Ошибка!</div>                    <!-- нативного нет -->
<div role="dialog" aria-modal="true">Модалка</div> <!-- лучше <dialog> -->
<div role="tablist">...</div>                       <!-- нативного нет -->
<div role="tab">...</div>
<div role="tabpanel">...</div>
<div role="progressbar" aria-valuenow="50">...</div>

Именование: aria-label, aria-labelledby, aria-describedby

aria-label -- текстовая метка

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

<!-- Навигация с именем -->
<nav aria-label="Основная навигация">
  <ul>...</ul>
</nav>

<!-- Поиск -->
<input type="search" aria-label="Поиск по сайту">

aria-labelledby -- ссылка на другой элемент

<!-- Связывает элемент с видимым заголовком -->
<section aria-labelledby="section-title">
  <h2 id="section-title">Новости</h2>
  <ul>...</ul>
</section>

<!-- Dialog -->
<dialog aria-labelledby="dialog-title">
  <h2 id="dialog-title">Подтверждение</h2>
  <p>Вы уверены?</p>
</dialog>

<!-- Несколько источников имени -->
<button aria-labelledby="action-text item-name">
  <span id="action-text">Удалить</span>
</button>
<span id="item-name">Товар #42</span>
<!-- Screen reader: "Удалить Товар #42" -->

aria-describedby -- дополнительное описание

<label for="pwd">Пароль:</label>
<input type="password" id="pwd"
       aria-describedby="pwd-hint pwd-error">
<p id="pwd-hint">Минимум 8 символов, одна цифра, одна заглавная</p>
<p id="pwd-error" role="alert"></p>
<!-- Screen reader: "Пароль, поле ввода. Минимум 8 символов..." -->

aria-hidden -- скрыть от screen reader

<!-- Декоративная иконка (скрыта от SR) -->
<button>
  <svg aria-hidden="true"><!-- иконка --></svg>
  Сохранить
</button>

<!-- Декоративный разделитель -->
<span aria-hidden="true">|</span>

<!-- Дублирующий текст -->
<a href="/cart">
  <span aria-hidden="true">🛒</span>
  Корзина
</a>

Никогда не ставь aria-hidden="true" на фокусируемый или интерактивный элемент.

aria-live -- динамические обновления

<!-- Вежливое уведомление (читается после текущей фразы) -->
<div aria-live="polite" id="status">
  <!-- JS обновит содержимое, SR прочитает -->
</div>

<!-- Важное уведомление (прерывает текущее чтение) -->
<div aria-live="assertive" id="error">
  <!-- Критические ошибки -->
</div>

<!-- role="alert" = aria-live="assertive" + aria-atomic="true" -->
<div role="alert" id="form-error"></div>

<!-- role="status" = aria-live="polite" + aria-atomic="true" -->
<div role="status" id="search-results">Найдено 42 результата</div>
// Динамическое обновление -- SR объявит
document.getElementById('status').textContent = 'Данные сохранены';
document.getElementById('error').textContent = 'Ошибка подключения';

aria-expanded -- раскрываемые элементы

<!-- Аккордеон -->
<button aria-expanded="false" aria-controls="panel-1" id="btn-1">
  Раздел 1
</button>
<div id="panel-1" role="region" aria-labelledby="btn-1" hidden>
  Содержимое раздела 1
</div>

<script>
  const btn = document.getElementById('btn-1');
  const panel = document.getElementById('panel-1');

  btn.addEventListener('click', () => {
    const isExpanded = btn.getAttribute('aria-expanded') === 'true';
    btn.setAttribute('aria-expanded', !isExpanded);
    panel.hidden = isExpanded;
  });
</script>

Другие важные ARIA-атрибуты

<!-- aria-controls: элемент, которым управляет текущий -->
<button aria-controls="menu" aria-expanded="false">Меню</button>
<nav id="menu" hidden>...</nav>

<!-- aria-current: текущий элемент в наборе -->
<nav>
  <a href="/" aria-current="page">Главная</a>
  <a href="/about">О нас</a>
</nav>

<!-- aria-disabled: визуально отключён, но в фокусе -->
<button aria-disabled="true">Недоступно</button>

<!-- aria-busy: контент загружается -->
<div aria-busy="true" aria-live="polite">
  Загрузка данных...
</div>

<!-- aria-required: обязательное поле (дополняет required) -->
<input type="text" required aria-required="true">

<!-- aria-invalid: поле невалидно -->
<input type="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error" role="alert">Введите корректный email</p>

<!-- aria-selected: выбранный элемент -->
<div role="tab" aria-selected="true">Вкладка 1</div>

<!-- aria-pressed: кнопка-переключатель -->
<button aria-pressed="false">Тёмная тема</button>

Пример: табы с ARIA

<div role="tablist" aria-label="Вкладки настроек">
  <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">
    Профиль
  </button>
  <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">
    Безопасность
  </button>
  <button role="tab" id="tab-3" aria-selected="false" aria-controls="panel-3" tabindex="-1">
    Уведомления
  </button>
</div>

<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
  Содержимое профиля
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
  Содержимое безопасности
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
  Содержимое уведомлений
</div>

Частые ошибки

Ошибка Почему плохо Как правильно
ARIA вместо нативного HTML Лишняя работа, хуже поддержка <button> вместо <div role="button">
aria-hidden="true" на фокусируемом Элемент невидим для SR, но доступен Tab-ом Убери фокусируемость или aria-hidden
Нет aria-label на иконке-кнопке SR прочитает пустоту aria-label="Описание"
aria-live с большим контентом SR прочитает весь блок Обновляй маленький фрагмент
role без поддержки клавиатуры Элемент "кнопка" не нажимается Enter/Space Добавь keyboard handlers
aria-expanded без обновления SR не знает текущее состояние Обновляй через JS

Практика

  1. Создай кнопку-иконку с aria-label и проверь через screen reader
  2. Реализуй аккордеон с aria-expanded и aria-controls
  3. Добавь aria-live="polite" region и обнови его через JS -- послушай SR
  4. Создай навигацию с aria-current="page" на текущей странице
  5. Проверь свою страницу через axe DevTools -- исправь ARIA-ошибки

Связанные темы

Ресурсы