Доступность уровня production
Основы ты уже знаешь. Здесь — то, что начинается дальше: ARIA-паттерны, управление фокусом, живые области и как это проверить самому.
Что ты научишься делать
- Соберёшь модальное окно, полностью доступное с клавиатуры
- Научишься сообщать о динамических изменениях через живые области
- Проверишь свой интерфейс скринридером — на своём же компьютере
#Что уже пройдено
В десятом уроке основ разобраны фундаментальные вещи: семантика, alt, видимый фокус, контраст, порядок заголовков, skip-link. Повторять не будем.
Этого хватает для страницы с текстом. Не хватает — как только появляется интерфейс: выпадающее меню, вкладки, модальное окно, живой счётчик корзины. Всё, у чего есть состояние.
#Из чего складывается доступное имя
Читалка объявляет элемент по формуле: имя, роль, состояние. «Удалить, кнопка, недоступна».
Имя берётся из первого найденного источника — и порядок здесь важен, потому что aria-label перекрывает видимый текст.
| Приоритет | Источник | Замечание |
|---|---|---|
| 1 | aria-labelledby | Ссылка на id другого элемента. Побеждает всё |
| 2 | aria-label | Перекрывает видимый текст — легко случайно скрыть настоящее имя |
| 3 | Связанный <label> | Для полей формы |
| 4 | Текст внутри элемента | Обычный случай для кнопок и ссылок |
| 5 | title | Работает, но ненадёжно: на сенсорных экранах не показывается |
Так не надо
<button aria-label="Кнопка">
Оформить заказ
</button>Читалка произнесёт «Кнопка», а не «Оформить заказ». Видимый текст перекрыт бесполезной подписью — и человек не сможет назвать команду голосом.
Так надо
<button>Оформить заказ</button>
<button aria-label="Закрыть окно">
<svg aria-hidden="true">…</svg>
</button>Есть текст — <code>aria-label</code> не нужен. Нет текста, только иконка — вот тогда он и ставится.
#Управление фокусом
Это главное, что отличает доступный интерфейс от недоступного. Правило простое: фокус должен идти туда, куда пошло внимание.
Открылось модальное окно — фокус внутрь. Закрылось — обратно на кнопку, которая его открыла. Появилось сообщение об ошибке — фокус на первое поле с ошибкой.
<button type="button" data-open-modal>Удалить заказ</button><div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" hidden> <h2 id="modal-title">Удалить заказ?</h2> <p>Отменить это действие нельзя.</p> <button type="button" data-confirm>Удалить</button> <button type="button" data-cancel>Отмена</button></div>Разметка — половина дела. Вторая половина: перевести фокус внутрь, не выпускать его наружу, пока окно открыто, и вернуть на место при закрытии.
Запомни, откуда пришли
Перед открытием сохрани
document.activeElement. Иначе возвращать фокус будет некуда.Переведи фокус внутрь
На первый интерактивный элемент или на сам диалог. Не на кнопку подтверждения удаления — человек может нажать её вслепую.
Запри фокус
Пока окно открыто, Tab не должен уводить на страницу под ним. Это и называется фокус-ловушкой.
Дай выход по Escape
Обязательно. Это единственный способ закрыть окно, если человек не видит крестик.
Верни фокус
При закрытии — на сохранённый элемент. Без этого фокус улетает в начало страницы, и человек теряет место.
#Живые области
Зрячий человек видит, что счётчик корзины изменился. Читалка молчит: она озвучивает только то, на чём фокус.
aria-live помечает область, изменения в которой нужно объявлять вслух.
| Значение | Когда объявит | Для чего |
|---|---|---|
polite | Когда читалка закончит текущую фразу | Почти всё: счётчики, результаты поиска, «сохранено» |
assertive | Немедленно, перебивая | Только критичное: ошибка оплаты, потеря связи |
off | Не объявлять | Значение по умолчанию |
<!-- Область объявлена ЗАРАНЕЕ и пуста. Если создать её вместе с текстом, объявления не будет --><p class="status" role="status" aria-live="polite"></p><!-- Для критичного — отдельная область --><p class="error" role="alert"></p><script> // Читалка объявит «Сохранено» после текущей фразы document.querySelector('.status').textContent = 'Сохранено';</script>#Скрывать правильно
Три способа спрятать элемент, и они не взаимозаменяемы.
| Способ | Видно глазами | Слышно читалке | Доступно с клавиатуры |
|---|---|---|---|
display: none или hidden | Нет | Нет | Нет |
aria-hidden="true" | Да | Нет | Да — и это ловушка |
Класс visually-hidden | Нет | Да | Да |
opacity: 0, visibility: hidden | Нет | Зависит | Зависит |
.visually-hidden { position: absolute; inline-size: 1px; block-size: 1px; padding: 0; margin: -1px; overflow: hidden; /* clip-path вместо устаревшего clip */ clip-path: inset(50%); white-space: nowrap;}#Как проверить самому
Скринридер уже стоит на твоём компьютере — включается сочетанием клавиш. Полчаса с ним заменяют десяток статей.
| Система | Программа | Включение |
|---|---|---|
| macOS | VoiceOver | ⌘ + F5 |
| Windows | NVDA (бесплатно) или встроенный «Экранный диктор» | Ctrl + Win + Enter |
| Android | TalkBack | Настройки → Специальные возможности |
| iOS | VoiceOver | Настройки → Универсальный доступ |
Что слушать в первую очередь: пройди страницу по заголовкам, потом по ссылкам, потом заполни форму. Три вопроса — понятно ли, где ты находишься; понятно ли, что делает каждая кнопка; сообщается ли о том, что изменилось.
#Как это выглядит в реальном проекте
<div class="tabs"> <div role="tablist" aria-label="Информация о товаре"> <button role="tab" id="tab-desc" aria-selected="true" aria-controls="panel-desc"> Описание </button> <button role="tab" id="tab-spec" aria-selected="false" aria-controls="panel-spec" tabindex="-1"> Характеристики </button> </div> <div role="tabpanel" id="panel-desc" aria-labelledby="tab-desc">…</div> <div role="tabpanel" id="panel-spec" aria-labelledby="tab-spec" hidden>…</div></div>Обрати внимание на tabindex="-1" у неактивной вкладки. Это не ошибка: в наборе вкладок клавиша Tab должна перескакивать весь набор целиком, а переключение между вкладками идёт стрелками. Так работают вкладки во всех настольных программах — и того же ждут от сайта.
#Когда НЕ надо
- Вешать роли на семантические теги.
<nav role="navigation">— избыточно, роль уже есть. Лишние атрибуты только засоряют. - Дублировать текст в
aria-label. Это перекрывает видимое имя, и человек не сможет назвать кнопку голосом так, как её видит. - Ставить
aria-live="assertive"везде. Перебивать читалку — это как кричать. Для почти всего подходитpolite. - Полагаться на автоматические проверки. Они ловят примерно треть проблем: пропущенный
alt, слабый контраст. Понятна ли кнопка на слух — не проверит ни один инструмент.
#Поддержка браузерами
| Возможность | Состояние |
|---|---|
| Атрибуты ARIA | Везде; поведение слегка различается между читалками |
<dialog> и showModal() | Во всех современных браузерах |
::backdrop | Вместе с dialog |
inert | Во всех современных браузерах — удобная замена ручной фокус-ловушке |
:focus-visible | Во всех современных браузерах |
Где спотыкаются почти все
Фокус не переводится в открытое окно
Почему Окно открылось, но Tab продолжает ходить по странице под ним. Человек не понимает, что произошло.
Как надо Переводи фокус внутрь при открытии и возвращай при закрытии. С <dialog> это делается само.
Живая область создаётся вместе с текстом
Почему Читалка следит за изменениями существующей области. Новый элемент с текстом она не заметит.
Как надо Пустой контейнер с aria-live должен быть в разметке заранее.
<code>aria-hidden</code> на фокусируемом элементе
Почему Элемент остаётся в обходе клавиатурой, но не объявляется. Фокус попадает в тишину.
Как надо Добавь tabindex="-1" или используй hidden.
Все вкладки доступны по Tab
Почему В наборе из шести вкладок человек шесть раз жмёт Tab, чтобы пройти мимо. Ожидается один.
Как надо Активная вкладка в обходе, остальные с tabindex="-1", переключение стрелками.
- Доступное имя accessible name
- Как читалка называет элемент. Собирается по приоритету источников.
- Фокус-ловушка focus trap
- Удержание фокуса внутри окна, пока оно открыто.
- Живая область live region
- Область, изменения в которой читалка объявляет вслух.
- Инертность inert
- Атрибут, выключающий ветку разметки из взаимодействия целиком.
- Скрытие визуальное visually hidden
- Приём: элемент не виден глазами, но доступен читалке.
Проверь себя
<code>aria-label</code> перекрывает видимый текст. Это частая ошибка: настоящее имя оказывается скрытым.
Пустой контейнер объявляется заранее, а текст в него подставляется потом.
Нужно добавить <code>tabindex="-1"</code> либо использовать <code>hidden</code>.
Активная вкладка в обходе, остальные с <code>tabindex="-1"</code>. Так устроены вкладки во всех настольных программах.
Это тот случай, когда готовый тег закрывает почти всё, ради чего раньше писали сотню строк скрипта.
Задание
Доступное модальное окно
Модальное окно собрано из div и недоступно: нет роли, нет связи с заголовком, кнопка-крестик без имени, а иконка внутри неё озвучивается.
Почини разметку: дай окну роль dialog с aria-modal, свяжи с заголовком через aria-labelledby, сделай крестик настоящей кнопкой с aria-label, спрячь иконку через aria-hidden. Добавь в CSS видимую обводку фокуса.
Подсказка 1
Окну: <div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
Подсказка 2
Заголовку: <h2 class="modal__title" id="modal-title">Удалить заказ?</h2>
Подсказка 3
Крестик: <button class="modal__close" aria-label="Закрыть окно"> и внутри <svg aria-hidden="true">
Подсказка 4
В CSS замени :focus { outline: none; } на :focus-visible { outline: 3px solid #6544e0; outline-offset: 2px; }
Показать решение
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title"> <h2 class="modal__title" id="modal-title">Удалить заказ?</h2> <p class="modal__text">Отменить это действие нельзя.</p> <button type="button" class="modal__close" aria-label="Закрыть окно"> <svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"> <path d="m6 6 12 12M18 6 6 18" stroke="currentColor" stroke-width="2"/> </svg> </button> <button class="modal__btn">Удалить</button></div>.modal { position: relative; max-width: 320px; padding: 20px; background: #fff; border-radius: 14px; box-shadow: 0 20px 50px -20px rgb(20 24 48 / .5);}.modal__title { margin: 0 0 8px; font-size: 18px; }.modal__text { margin: 0 0 16px; font-size: 14px; color: #5b6070; }.modal__close { position: absolute; top: 12px; right: 12px; padding: 4px; background: transparent; border: 0; border-radius: 8px; cursor: pointer;}.modal__btn { padding: 9px 16px; font: inherit; color: #fff; background: #c8394a; border: 0; border-radius: 999px;}:focus-visible { outline: 3px solid #6544e0; outline-offset: 2px;}