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
- Не используй ARIA, если есть нативный HTML-элемент
- Не меняй нативную семантику (не ставь
role="heading"на<div>если можно<h2>) - Все интерактивные ARIA-элементы должны быть доступны с клавиатуры
- Не используй
role="presentation"илиaria-hidden="true"на фокусируемых элементах - Все интерактивные элементы должны иметь доступное имя
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 |
Практика
- Создай кнопку-иконку с
aria-labelи проверь через screen reader - Реализуй аккордеон с
aria-expandedиaria-controls - Добавь
aria-live="polite"region и обнови его через JS -- послушай SR - Создай навигацию с
aria-current="page"на текущей странице - Проверь свою страницу через axe DevTools -- исправь ARIA-ошибки
Связанные темы
- Семантическая разметка -- нативная семантика важнее ARIA
- Семантика для screen readers -- как SR воспринимает ARIA
- Фокус и клавиатурная навигация -- клавиатурная поддержка для ARIA
- dialog -- нативный элемент вместо role="dialog"
- details и summary -- нативный аккордеон вместо aria-expanded