Живые области (aria-live)
Скринридер читает то, где стоит курсор. Всё, что меняется в другом углу экрана, для него не существует — если вы заранее не пометили это место живой областью.
Живая область (live region) — участок страницы, изменения внутри которого скринридер объявляет вслух сам, не перемещая туда фокус пользователя.
Представьте: пользователь заполняет форму заказа, жмёт «Оплатить», и под кнопкой красным появляется «Карта отклонена». Зрячий человек видит это боковым зрением за долю секунды. Слепой не видит ничего: его виртуальный курсор стоит на кнопке, а текст возник в другом узле DOM. Он снова жмёт «Оплатить». И снова. Пока не уйдёт с сайта.
Зачем это на практике
Живые области нужны везде, где интерфейс говорит с пользователем без перезагрузки страницы:
- ошибки и подтверждения в формах («Пароль слишком короткий», «Заказ оформлен»);
- тосты и всплывающие уведомления («Черновик сохранён»);
- счётчик результатов поиска и фильтров («Найдено 12 товаров»);
- индикаторы загрузки («Загружаем список…»);
- изменение содержимого корзины, таймер сессии, новое сообщение в чате.
Это редкий случай, когда ARIA не просто подписывает, а действительно заставляет что-то произойти: браузер начинает следить за поддеревом и передавать изменения вспомогательным технологиям.
Как объявить живую область
Два способа: атрибут aria-live или роль, которая включает его неявно.
<!-- Явно -->
<div id="form-status" aria-live="polite"></div>
<!-- Через роль: то же самое, но выразительнее -->
<div id="form-status" role="status"></div>| Роль | Эквивалент | Для чего |
role="status" | aria-live="polite" + aria-atomic="true" | Обычные сообщения: «Сохранено», «Найдено 12 товаров» |
role="alert" | aria-live="assertive" + aria-atomic="true" | Срочное: ошибка оплаты, потеря соединения |
role="log" | aria-live="polite" | Лента, где важен порядок: чат, консоль |
Роли предпочтительнее: они короче и заодно сообщают о характере области, а не только о вежливости.
polite против assertive
polite — «скажи, когда договоришь». Скринридер дочитает текущую фразу и только потом озвучит сообщение. assertive — «перебей немедленно»: текущая речь обрывается на полуслове.
Правило простое: assertive только тогда, когда пользователь не может продолжать работу, не узнав об этом. Ошибка оплаты, разорванная сессия, потеря несохранённых данных. Всё остальное — polite. Интерфейс, который перебивает вас каждые три секунды, невозможно использовать; это ровно то же чувство, что и всплывающая поверх текста реклама.
Живая область должна существовать заранее
Это ошибка номер один, и она стоит того, чтобы выделить её в отдельный раздел. Браузер объявляет только те изменения, которые произошли внутри уже наблюдаемой области. Если вы создаёте div с aria-live и текстом одним куском и вставляете его в DOM — скринридер, скорее всего, промолчит: в момент появления области менять в ней было нечего.
// Плохо: контейнер и текст появляются одновременно — тишина
const box = document.createElement('div');
box.setAttribute('aria-live', 'polite');
box.textContent = 'Заказ оформлен';
document.body.append(box);Правильно так: пустой контейнер лежит в разметке с самого начала, а JavaScript кладёт в него текст.
<!-- Ждёт своего часа с момента загрузки страницы -->
<div id="form-status" role="status"></div>const status = document.getElementById('form-status');
function announce(message) {
status.textContent = ''; // сбрасываем предыдущее
// отдаём браузеру кадр, чтобы он заметил именно ИЗМЕНЕНИЕ
requestAnimationFrame(() => {
status.textContent = message;
});
}
announce('Заказ оформлен, номер 4417');Сброс через пустую строку решает вторую типичную беду: одинаковый текст подряд. Если пользователь дважды ввёл неверный код и оба раза в области оказалось «Неверный код», часть скринридеров второй раз не скажет ничего — текст-то не изменился. Очистка и повторная запись создают честное изменение.
Три живых примера
Ошибка поля формы
Сообщение об ошибке привязывают к полю через aria-describedby, а сам контейнер делают живой областью. Тогда ошибка будет и объявлена сразу, и прочитана повторно, когда пользователь вернётся к полю.
<label for="email">Электронная почта</label>
<input id="email" type="email" aria-describedby="email-err" aria-invalid="true">
<p id="email-err" role="alert">Введите адрес в формате [email protected]</p>Счётчик результатов
Фильтры в каталоге меняют выдачу без перезагрузки. Зрячий видит, что товаров стало меньше; остальным нужно об этом сказать.
<div role="status">Найдено 12 товаров</div>Если фильтр — это поле ввода, объявление на каждое нажатие клавиши превращается в пытку. Ставьте задержку: отправляйте сообщение через 500–1000 мс после последнего ввода, когда результат устоялся.
Тост «Черновик сохранён»
Тост живёт пару секунд и исчезает. Держите на странице постоянный контейнер-«сцену» с role="status", а внутрь подставляйте и убирайте текст. Убирать сам контейнер нельзя — вместе с ним пропадёт и наблюдение.
Сообщение, которого не видно
Часто объявление не нужно показывать глазами: страница и так перерисовала выдачу, и подпись «Найдено 12 товаров» просто продублирует то, что видно. Соблазн — спрятать область через display: none. Так нельзя: узел, скрытый этим способом, выпадает из дерева доступности вместе со всеми будущими объявлениями. Прячьте визуально, оставляя элемент в дереве.
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}Этот класс — рабочая лошадка доступности: им же скрывают подписи полей и уточнения к ссылкам. Проверять живые области обязательно на настоящем скринридере — VoiceOver на macOS или NVDA на Windows. Ни axe, ни Lighthouse не скажут вам, объявилось сообщение или растворилось в тишине: автотесты видят атрибут, но не слышат речь.
Как это работает
Когда браузер строит дерево доступности, он помечает узлы с aria-live как живые. Дальше он следит за мутациями внутри них. Изменился текст — браузер отправляет вспомогательной технологии событие с новым содержимым, и скринридер ставит его в очередь: в конец (polite) или в начало с прерыванием (assertive).
Что именно попадёт в очередь, решает aria-atomic. По умолчанию (false) объявляется только изменившийся узел. С aria-atomic="true" — вся область целиком. Разница видна на счётчике: если в области лежит текст «Осталось 5 попыток» и меняется только цифра, без aria-atomic пользователь услышит одинокое «4» без контекста.
Есть ещё aria-busy="true": он говорит «я сейчас перестраиваю содержимое, подожди объявлять». Ставите перед серией правок, снимаете после — и вместо десяти обрывков пользователь получит одно осмысленное сообщение.
Частые ошибки
- Область создаётся вместе с сообщением. Самая частая причина «aria-live не работает». Контейнер должен быть в DOM заранее.
- assertive на всё подряд. Спиннер загрузки с
role="alert", который тикает каждые полсекунды, перебивает вообще всю речь. - Живая область спрятана через
display: noneилиvisibility: hidden. Содержимое не объявляется. Прячьте текст визуально классом сclip-path(visually-hidden), но не убирайте его из дерева доступности. - Пять живых областей на одной странице. Они начинают конкурировать за очередь. Хватает двух: одна polite и одна assertive.
- Дублирование. Если сообщение об ошибке объявлено живой областью и туда же переводится фокус, пользователь услышит его дважды. Выбирайте что-то одно.
- Живая область вместо фокуса. Открылось модальное окно — фокус переводят внутрь, а не зачитывают окно через aria-live.
Итоги
- Живая область — единственный способ сообщить об изменении, не трогая фокус.
- Контейнер должен лежать в DOM до изменения, иначе объявления не будет.
role="status"для обычного,role="alert"— только для того, что мешает продолжать работу.- Одинаковый текст подряд не объявляется: очищайте область перед повторной записью.
aria-atomic="true"заставляет читать область целиком,aria-busy— переждать серию правок.- Скрывать живую область через
display: noneнельзя — используйте визуальное скрытие.