Пять правил ARIA (и главное из них)
ARIA умеет переименовать элемент для скринридера, но не умеет научить его работать. Отсюда растут все пять правил — и все беды тех, кто их не знает.
ARIA (Accessible Rich Internet Applications) — набор HTML-атрибутов, который правит представление элемента в дереве доступности: его роль, имя и состояние. Ни внешний вид, ни поведение элемента ARIA не меняет.
Фраза «не меняет поведение» звучит как формальность, пока не увидишь последствия. Вы пишете <div role="button">Купить</div>. Скринридер честно объявляет: «Купить, кнопка». Пользователь нажимает Enter — ничего не происходит. Точнее, фокус на этот div даже не встаёт: до Enter дело не доходит. Роль пообещала кнопку, а кнопки нет.
ARIA — это подпись на коробке, а не содержимое коробки. Подпись читают вспомогательные технологии: скринридеры (NVDA, JAWS, VoiceOver), голосовое управление, брайлевские дисплеи. Браузер на неё почти не реагирует: он не добавит обработчик клавиш, не сделает элемент фокусируемым, не изменит курсор. Всё это остаётся на вас.
Зачем это на практике
Современный макет собирается из <div>. Дизайнер прислал карточку с «кнопкой» — прямоугольник со скруглением; вёрстку делают дивом, потому что так проще победить стили. Через полгода приходит требование доступности, и у команды два пути: вернуть нативные элементы или подписать существующие дивы атрибутами ARIA.
Первый путь почти всегда дешевле. Нативная <button> бесплатно приносит фокус по Tab, срабатывание по Enter и Space, роль «кнопка», состояние «недоступна» при disabled, участие в форме и поведение в режиме высокой контрастности Windows. Всё это придётся написать руками, если вы выбрали div.
ARIA нужна там, где в HTML просто нет подходящего тега: вкладки, комбобокс с автодополнением, дерево, слайдер с двумя ручками, всплывающая подсказка, область, куда прилетают новые сообщения. Это язык для того, чего HTML не умеет, а не запасной способ описать то, что он умеет.
Пять правил ARIA
Правила сформулированы рабочей группой W3C в документе «Using ARIA». Порядок не случайный: первое весит больше остальных четырёх вместе.
Правило 1. Не используй ARIA, если есть нативный элемент
Если существует HTML-элемент с нужной семантикой и поведением — берите его, даже если придётся повоевать со стилями.
<!-- Плохо: три атрибута и обработчики впереди -->
<div role="button" tabindex="0" onkeydown="...">Отправить</div>
<!-- Хорошо: то же самое, но всё работает из коробки -->
<button type="submit">Отправить</button>Обычно возражают: «нативную кнопку не застилизуешь». Это миф родом из 2010-х. Четыре строки CSS — и <button> выглядит как угодно.
button.reset {
all: unset;
cursor: pointer;
display: inline-flex;
align-items: center;
}Правило 2. Не переопределяй нативную семантику без крайней нужды
Роль ARIA не добавляется к роли элемента — она её заменяет. Заголовок с ролью вкладки перестаёт быть заголовком: он пропадает из списка заголовков, по которому слепые пользователи ориентируются на странице (в NVDA это клавиша H).
<!-- Плохо: заголовок исчез из структуры страницы -->
<h2 role="tab">Доставка</h2>
<!-- Хорошо: роль на отдельном элементе внутри -->
<h2><button role="tab" aria-selected="true">Доставка</button></h2>Самый разрушительный случай — role="presentation" (или его синоним role="none") на таблице с данными. Он снимает роль таблицы вместе со всей навигацией по строкам и столбцам: пользователь больше не может спросить «в какой я колонке?». Ставьте его только на таблицы, которые используются для раскладки (в письмах такое ещё встречается).
Правило 3. Любой интерактивный ARIA-виджет обязан работать с клавиатуры
Если вы назначили роль button, tab, menuitem, checkbox — вы взяли на себя обязательство реализовать клавиатуру: фокус через tabindex, нужные клавиши, видимое кольцо фокуса. Роль без клавиатуры — обещание, которое элемент не может выполнить.
Правило 4. Не прячь от скринридера то, на что можно поставить фокус
Ни aria-hidden="true", ни role="presentation" не должны стоять на фокусируемом элементе или его предке. Иначе фокус приходит на элемент, которого для скринридера не существует, и пользователь слышит тишину. Об этом «фантомном фокусе» — отдельный урок в конце раздела.
Правило 5. У каждого интерактивного элемента должно быть доступное имя
Имя — то, что скринридер произносит вслух. У кнопки с текстом имя берётся из содержимого. У иконочной кнопки содержимого нет, и без явного имени она озвучится как «кнопка» — угадывайте, что она делает.
<!-- Плохо: имени нет, скринридер скажет просто «кнопка» -->
<button><svg aria-hidden="true">…</svg></button>
<!-- Хорошо: иконка скрыта, имя задано текстом -->
<button aria-label="Удалить комментарий">
<svg aria-hidden="true" focusable="false">…</svg>
</button>Роли, состояния и свойства
Атрибуты ARIA делятся на три группы, и путаница между ними — источник половины ошибок.
| Группа | Пример | Что описывает | Меняется во времени? |
| Роль | role="tab", role="dialog" | Что это за штука | Практически никогда |
| Свойство | aria-label, aria-controls, aria-required | Устойчивую характеристику | Редко |
| Состояние | aria-expanded, aria-checked, aria-disabled, aria-invalid | Текущее положение дел | Постоянно — это ваша работа |
Состояние без JavaScript мертво. Аккордеон, у которого aria-expanded="false" прописано в разметке и никогда не обновляется, врёт пользователю на каждом клике: панель открыта, а скринридер продолжает объявлять «свёрнуто».
<button aria-expanded="false" aria-controls="panel-1">Условия доставки</button>
<div id="panel-1" hidden>
<p>Доставим за 2 дня по городу.</p>
</div>const btn = document.querySelector('[aria-controls="panel-1"]');
const panel = document.getElementById('panel-1');
btn.addEventListener('click', () => {
const open = btn.getAttribute('aria-expanded') === 'true';
btn.setAttribute('aria-expanded', String(!open));
panel.hidden = open;
});Здесь важна пара: aria-expanded висит на кнопке, а не на панели, и указывает на панель через aria-controls. Атрибут на панели скринридер просто не увидит — пользователь стоит на кнопке.
aria-label против aria-labelledby
Оба атрибута задают доступное имя, но по-разному.
| aria-label | aria-labelledby | |
| Что принимает | Строку текста | Один или несколько id через пробел |
| Видно на экране | Нет | Да — берётся из существующего текста |
| Когда брать | Видимой подписи нет (иконка, лендмарка) | Подпись уже есть на странице |
| Приоритет | Ниже | Выше — перебивает aria-label |
<!-- Имя собирается из двух узлов: «Удалить Отчёт за март» -->
<h3 id="file-7">Отчёт за март</h3>
<button id="del-7" aria-labelledby="del-7 file-7">Удалить</button>Приём с перечислением нескольких id (включая собственный) спасает списки, где двадцать одинаковых кнопок «Удалить»: скринридер прочтёт каждую вместе с именем файла.
Правило выбора: если подпись видна глазами — aria-labelledby. Если подписи нет вообще — aria-label. Порядок вычисления имени такой: aria-labelledby, затем aria-label, затем нативная подпись (<label>, alt, содержимое элемента), затем title — как последняя надежда.
Отдельная ловушка: aria-label не должен противоречить видимому тексту. Кнопка с надписью «Отправить» и aria-label="Сохранить черновик" ломает голосовое управление — пользователь говорит «нажми Отправить», и система его не понимает, потому что для неё элемент называется иначе. Это нарушение критерия WCAG 2.5.3 «Label in Name»: видимый текст должен входить в доступное имя.
Как это работает
Браузер строит из DOM второе дерево — дерево доступности. В нём остаются только смысловые узлы, и у каждого есть роль, имя, состояние, значение. Скринридер читает именно его, а не вашу вёрстку.
ARIA-атрибуты вмешиваются ровно на этом шаге: они правят поля узла. Роль перезаписывает роль, aria-label перезаписывает имя, aria-expanded добавляет состояние. Обработчики событий, порядок фокуса, стили — всё это живёт в DOM и остаётся нетронутым.
Отсюда главный вывод урока: ARIA ничего не чинит. Она сообщает. Если сообщение не совпадает с реальностью — вы сделали хуже, чем если бы промолчали. В следующем уроке этого раздела разберём приятное исключение из правила «ARIA ничего не делает» — живые области, которые действительно заставляют скринридер заговорить.
Частые ошибки
- ARIA вместо HTML.
<div role="link">вместо<a href>: нет перехода по Enter, нет открытия в новой вкладке, нет статуса «посещённая». - aria-label на неинтерактивном элементе. На обычном
<div>или<span>без роли имя чаще всего игнорируется — у элемента нет роли, к которой его можно привязать. - Состояние задано в разметке и забыто в JS. Классика с
aria-expandedиaria-checked. - Опечатка в значении.
aria-expanded="True"илиaria-hidden="yes"— значения регистрозависимы, допустимы толькоtrueиfalse. - aria-labelledby на несуществующий id. Ссылка в никуда — имя пропадает совсем, и элемент становится безымянным.
- Дублирование имени.
<button aria-label="Поиск">Поиск</button>— атрибут просто лишний.
Итоги
- ARIA правит дерево доступности: роль, имя, состояние. Поведение остаётся на вас.
- Первое правило важнее остальных: есть нативный элемент — берите его.
- Роль ARIA заменяет нативную семантику, а не дополняет её.
- Роли и свойства статичны, состояния нужно синхронизировать в JavaScript при каждом изменении.
- Видимая подпись есть —
aria-labelledby; подписи нет —aria-label;aria-labelledbyперебиваетaria-label. - Имя не должно противоречить видимому тексту, иначе ломается голосовое управление.