Живые области (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 нельзя — используйте визуальное скрытие.
Проверьте себя
1. Разработчик создаёт в JS новый div с aria-live="polite" и текстом «Сохранено», добавляет его в body — и скринридер молчит. Почему?
AНужно было указать assertive: polite вообще не озвучивается
BУ div отсутствует tabindex, поэтому он не попал в дерево доступности
CОбласть должна существовать в DOM до изменения — браузер объявляет только изменения внутри уже наблюдаемой области
DТекст слишком короткий, скринридеры игнорируют сообщения короче трёх слов
2. Какое сообщение уместно объявить через aria-live="assertive" (role="alert")?
A«Черновик сохранён» после автосохранения
B«Найдено 12 товаров» после применения фильтра
C«Соединение потеряно, изменения не сохранены»
D«Загружаем список…» на время запроса