Монтиран на стена плосък високоговорител с концентрични нарисувани вълнообразни линии, разпространяващи се навън, като най-вътрешният пръстен е в алено червено — визуалният маркер за инфраструктурата на оповестяванията чрез live региони през различните рамки.
Image description: Монтиран на стена плосък високоговорител с концентрични нарисувани вълнообразни линии, разпространяващи се навън, като най-вътрешният пръстен е в алено червено — визуалният маркер за инфраструктурата на оповестяванията чрез live региони през различните рамки.

Инженерен наръчник · aria-live през рамките

aria-live региони в React, Vue, Svelte и SolidJS: какво работи и какво не

Тествахме aria-live региони в React 19, Vue 3.5, Svelte 5 и SolidJS 2.0 — четири канонични модела, три екранни четеца, всеки специфичен за рамката капан. Ето матрицата на поведението, добрия срещу лошия код и наръчника.

aria-live региони в React, Vue, Svelte и SolidJS:
какво работи и какво не

Взехме четирите канонични aria-live модела — toast известия, оповестяване на грешки във формуляри, асинхронни състояния на зареждане и обновяване на данни на живо — и ги прекарахме през React 19, Vue 3.5, Svelte 5 и SolidJS 2.0 срещу NVDA 2025.1, JAWS 2026 и VoiceOver на macOS 15.4. Добрата новина: всяка рамка може да се справи. Лошата новина: всяка от тях нарушава спецификацията по различен начин, а режимите на отказ не са преносими.

4
тествани рамки
12
комбинации рамка-модел
3
проверени екранни четеца
14 мин. четене
Обновено май 2026 г.

1. Какво всъщност е aria-live — и какво всъщност прави браузърът с него

aria-live регион е DOM възел, чийто атрибут aria-live обещава на клиента на помощната технология, че промените в текста на подчинените елементи на възела ще бъдат оповестени в момента на настъпването им — без потребителят да трябва да премества фокуса, за да ги прочете. Стойностите са polite (оповести, когато потребителят е бездействащ), assertive (прекъсни текущата реч) и off (стойността по подразбиране).

Механизмът, който всъщност задвижва оповестяването, е дървото на достъпността, което браузърът изгражда от рендирания DOM. Когато текстовото съдържание на live регион се промени, браузърът активира мутация, платформеният интерфейс за достъпност я наблюдава, а екранният четец произнася новия текст. Нищо от това няма общо с вашата рамка. Единствената задача на рамката е да направи така, че DOM да изглежда по начина, по който спецификацията предполага, че ще изглежда, в момента, в който спецификацията предполага, че ще изглежда така.

Тази последна уговорка е мястото, където всяка рамка изпада в затруднение. Спецификацията ARIA на W3C предполага синхронен, императивен модел на мутация на DOM. React 19, Vue 3.5, Svelte 5 и SolidJS 2.0 планират записите в DOM чрез собствените си реконсилиатори — и планировчикът на всеки от тях има различни инварианти. Резултатът: едно и също aria-live маркиране, написано по един и същи начин, може да се задейства надеждно в една рамка и безшумно да се провали в друга.

Спецификация срещу реализация

ARIA 1.3 (април 2025 г.) изясни, че потребителските агенти трябва да наблюдават мутациите в live регион веднага щом приключи заобикалящата микрозадача — но не ограничи слоя на рамката. На практика екранните четци прилагат debounce при около 100–200 ms, което прикрива много грешки във времевото поведение на рамките, но и ги скрива от автоматизираните тестове.


2. Четирите канонични модела, които реално се появяват в продукционен код

Почти всеки aria-live регион, който ще напишете за година фронтенд работа, попада в една от четири категории. Извлякохме моделите от извадка от около 320 библиотеки с компоненти (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte и дългата опашка от вътрешни дизайн системи) и ги групирахме по предназначение.

Модел 1 · Toast известие
”Запазено”, “Копирано в клипборда”, известия за грешки
около 78% от проучените библиотеки с компоненти предлагат toast
Настройка на livepolite за потвърждения, assertive за грешки
РискБързото монтиране/демонтиране убива оповестяването
Модел 2 · Оповестяване на грешка във формуляр
Вградена валидация под поле
Изисквано от WCAG 3.3.1 в интерактивни формуляри
Настройка на livepolite, в съчетание с aria-describedby
РискРегионът се монтира само при грешка — „няма регион, няма оповестяване“
Модел 3 · Асинхронно състояние на зареждане
”Зареждане на резултати…”, индикатори за зареждане, скелетони
Приблизително половината от проучените библиотеки го допускат погрешно
Настройка на livepolite с роля status
РискТекстът се сменя твърде бързо — прочита се само крайното състояние
Модел 4 · Обновяване на данни на живо
Табла с резултати, чат съобщения, броячи на опашки
Най-трудният от четирите за правилно изпълнение
Настройка на livepolite с роля log или status
РискСерии от обновявания претоварват синтезатора — „изпускане на опашката“

3. Специфичните за рамките капани, по ред на честота

Всяка рамка има свой реконсилиатор, а реконсилиаторът е мястото, където aria-live регионите умират. Обобщението в четири реда:

React 19
Конкурентен рендер · автоматично групиране
Най-честият източник на сигнали „toast-ът не се озвучи“
КапанАвтоматичното групиране слива две извиквания на setState в едно прилагане, така че „отвори toast, после смени текста му“ може да попадне в DOM като една мутация, която екранният четец третира като първоначалното монтиране на неозвучен регион.
РешениеМонтирайте региона празен при първото рендиране, после запишете текста при следващото изрисуване с flushSync или забавяне чрез микрозадача.
Vue 3.5
Планировчик, базиран на реактивност · nextTick
По-фин — отказите изглеждат като „регионът се оповести, но с грешен текст“
КапанПланировчикът на Vue прилага обновяванията на DOM в следващата микрозадача след промяна на състоянието. Текст на зареждане, който е записан и веднага заменен в рамките на същия tick, достига DOM само в крайния си вид — междинният низ „зареждане“ никога не се наблюдава.
РешениеИзползвайте await nextTick() между двата записа; или съставете региона от shallowRef, който планировчикът не дедуплицира.
Svelte 5
Runes · реактивност по време на компилация
Различна форма на грешка — компилаторът е и проблемът, и решението
КапанSvelte 5 компилира четенето на $state в директни записи в DOM, които заобикалят всякакво групиране на ниво рамка. Това звучи идеално за aria-live, докато не осъзнаете, че компилаторът също дедуплицира последователни идентични записи — така че „Loading…“, последвано от „Loading…“, се свежда до една мутация на DOM.
РешениеДобавяйте невидим брояч към текста на live региона при всяко обновяване; или използвайте untrack със стойност-страж, за да принудите нова мутация.
SolidJS 2.0
Финозърнести сигнали · без VDOM
Най-близо до „просто работи“ от четирите — но има свой граничен случай
КапанГрафът на сигнали на Solid обновява DOM възлите синхронно при задействане на сигнал, което е чудесно за aria-live. Но сигналите, които се задействат в рамките на блок batch(), се отлагат, а toast библиотеките често използват batch, за да групират няколко промени на състоянието — така че мутацията на текста на региона може да попадне едновременно с превключването на display: none на родителя.
РешениеДръжте сигнала, който притежава live региона, извън всяко извикване на batch(); или използвайте untrack, за да прочетете сигнала и да запишете в DOM в отделна задача.
Често допускана грешка · и в четирите рамки

Не монтирайте условно самия aria-live регион. От гледна точка на екранния четец регион, монтиран в момента, в който текстът му се появява за първи път, е празен регион — а празните региони не оповестяват нищо. Монтирайте региона празен при стартиране на приложението и променяйте само текста в него. Всяка от посочените по-горе рамки се проваля, ако нарушите това правило, независимо кой капан на планировчика вече сте заобиколили.


4. Матрицата на съвместимост: рамка × модел × помощна технология

Прекарахме всеки модел през всяка рамка срещу три екранни четеца — NVDA 2025.1 на Windows 11, JAWS 2026 на Windows 11 и VoiceOver на macOS 15.4 — използвайки Chrome 138, Firefox 130 и Safari 17.6. Всяка клетка записва поведението, което наблюдавахме при около 20 опитни прогона на комбинация. „OK“ означава, че оповестяването се задейства надеждно с очаквания текст. „Частично“ означава, че се задейства при някои конфигурации, но не при всички. „Отказ“ означава, че поне един екранен четец безшумно е изпуснал оповестяването.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast известие (polite)ЧастичноOKOKOK
Toast известие (assertive)ЧастичноOKЧастичноOK
Грешка във формуляр (polite)OKOKOKOK
Асинхронно състояние на зарежданеЧастичноЧастичноОтказOK
Данни на живо — бавен потокOKOKOKOK
Данни на живо — серия (повече от 5/сек.)ОтказЧастичноОтказЧастично

Три наблюдения от матрицата. Първо, всяка рамка обработва модела за грешки във формуляр правилно по подразбиране — това е единственият каноничен модел, който не натоварва реконсилиатора, защото регионът се монтира при стартиране на приложението и текстът се променя веднъж на изпращане. Второ, всяка рамка се затруднява с пристъпите от данни на живо, защото никой клиентски планировчик не е достатъчно бърз, за да подаде мутациите към дървото на достъпността със скоростта, с която се задейства подлежащият сигнал. Трето, дедупликацията по време на компилация на Svelte 5 превръща модела на състоянието на зареждане в пълен отказ вместо в частичен — единственият от четирите, при който поведението по подразбиране е грешно.

Колоната на екранните четци също е важна. JAWS 2026 е най-строгият от трите по отношение на празните региони: той ще откаже да оповести регион, чийто текст се променя от „“ на „Запазено“, ако промяната попадне в същото изрисуване като монтирането на региона, в която и да е рамка. NVDA 2025.1 оповестява непоследователно за същия случай. VoiceOver на macOS 15.4 е най-снизходителен — обикновено оповестява дори монтиране с текст в едно и също изрисуване — но неговата снизходителност е скрила много грешки на рамките от разработчиците, които тестват само на Mac.

Универсалното решение

И в четирите рамки единствената намеса, която обръща най-много клетки от “Частично” на “OK”, е монтирането на един глобален, празен div aria-live=“polite” и един div aria-live=“assertive” в корена на приложението — и насочването на всяко оповестяване през тях чрез записване на текст в техния подчинен елемент. Това заобикаля всяко състезание при монтиране в реконсилиатора с един ход.


5. Добър срещу лош код във всяка рамка

Следните двойки показват грешния и правилния начин за написване на модела на състоянието на зареждане във всяка рамка. Избрахме състоянието на зареждане, защото това е моделът, при който матрицата показва най-много червено — и разликата между лош и добър код е най-голяма.

React 19 · не правете така
function LoadingState({ isLoading, results }) {
  return isLoading ? (
    <div role="status" aria-live="polite">
      Loading results...
    </div>
  ) : (
    <ResultsList items={results} />
  );
}

Регионът се монтира само докато трае зареждането. Автоматичното групиране на React може да приложи монтирането и демонтирането в същото изрисуване като пристигането на данните — а JAWS, NVDA и VoiceOver не са на едно мнение какво да правят с това. Краен ефект: „Loading…“ понякога се произнася, понякога не, без модел, видим за клиента.

React 19 · правете така
function LoadingState({ isLoading, results }) {
  const message = isLoading ? 'Loading results...' : '';
  return (
    <>
      <div role="status" aria-live="polite" class="sr-only">
        {message}
      </div>
      {!isLoading && <ResultsList items={results} />}
    </>
  );
}

Регионът се монтира при първото рендиране и остава монтиран. Рендерът на React може да групира колкото си иска — единственото, което се променя, е текстът в съществуващ регион, което е точно онова, което описва спецификацията.

Vue 3.5 · не правете така
<template>
  <div role="status" aria-live="polite">
    {{ status }}
  </div>
</template>

<script setup>
async function load() {
  status.value = 'Loading...';
  const data = await fetch('/api/results').then(r => r.json());
  status.value = `Loaded ${data.length} results`;
}
</script>

Двата записа в status могат да попаднат в един и същи tick на планировчика на Vue, ако мрежовият отговор е бърз (кеширан) — Vue ще дедуплицира и само крайният низ ще достигне DOM. Оповестяването „Loading…“ се губи безшумно.

Vue 3.5 · правете така
<script setup>
import { nextTick } from 'vue';

async function load() {
  status.value = 'Loading...';
  await nextTick();
  const data = await fetch('/api/results').then(r => r.json());
  status.value = `Loaded ${data.length} results`;
}
</script>

await nextTick() принуждава планировчика да приложи „Loading…“ в DOM, преди второто присвояване да бъде поставено в опашката. Екранният четец вижда две отделни мутации и оповестява всяка от тях.

Svelte 5 · не правете така
<script>
  let status = $state('');

  async function load() {
    status = 'Loading...';
    const data = await fetch('/api/results').then(r => r.json());
    status = `Loaded ${data.length} results`;
  }
</script>

<div role="status" aria-live="polite">{status}</div>

Компилаторът на Svelte 5 издава запис на DOM текст при всяка промяна на $state, но дедуплицира последователните идентични низове. Ако второ извикване на load() запише „Loading…“ отново, компилаторът не издава мутация — екранният четец не чува нищо при второто щракване.

Svelte 5 · правете така
<script>
  let status = $state('');
  let seq = $state(0);

  async function load() {
    seq += 1;
    status = `Loading... ${seq}`;
    const data = await fetch('/api/results').then(r => r.json());
    status = `Loaded ${data.length} results (${seq})`;
  }
</script>

<div role="status" aria-live="polite">{status}</div>

Броячът на последователност гарантира, че всеки запис е нов низ. Потребителят не чува числото — екранният четец го изглажда — но компилаторът е принуден да издаде различна мутация на DOM всеки път. Заобикалянето на дедупликацията е целият смисъл.

SolidJS 2.0 · не правете така
import { batch, createSignal } from 'solid-js';

const [status, setStatus] = createSignal('');
const [results, setResults] = createSignal([]);

async function load() {
  batch(() => {
    setStatus('Loading...');
    setResults([]);
  });
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

Сигналът status се обновява в рамките на batch() заедно със сигнала results. Solid отлага и двата записа в DOM, докато batch не приключи — а при бърз кеширан отговор „Loading…“ и „Loaded…“ могат да се приложат в една и съща микрозадача. Междинното оповестяване се губи.

SolidJS 2.0 · правете така
async function load() {
  setStatus('Loading...');
  // status signal fires immediately, outside any batch
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

Записът „Loading…“ се случва извън batch(), така че финозърнестият планировчик на Solid обновява DOM в момента, в който сигналът се задейства. Екранният четец вижда оповестяването преди мрежовото пътуване в двете посоки. Записът „Loaded“ може да остане в batch — оповестяването все пак се задейства, защото batch приключва синхронно около него.


6. Наръчникът, валиден за всички рамки

1

Монтирайте по един глобален live регион за всяко ниво на учтивост при стартиране на приложението

Рендирайте два празни div елемента — единият с aria-live=“polite”, другият с aria-live=“assertive” — в корена на приложението си, преди да се рендира който и да е маршрут. Всяко оповестяване в приложението записва в един от тези два региона. Това премахва състезанието при монтиране във всяка от посочените по-горе рамки.

2

Напишете малка услуга за оповестяване, която обвива глобалните региони

Изложете една-единствена функция — announce(message, politeness) — която намира съответния глобален регион и задава неговия textContent. Рамките може да ви дадат реактивна референция към региона, но услугата за оповестяване може просто да извика el.textContent = ” първо, а след това el.textContent = message в следващата задача, което принуждава мутация дори при идентични низове.

3

Ограничете пристъпните източници на данни до около 1 съобщение на 1500 ms

Ако източникът ви на данни може да се задейства повече от веднъж в секунда — табло с резултати, чат поток — синтезаторът на екранния четец не може да насмогне, независимо от рамката. Обединете обновяванията от страна на клиента и издайте едно обобщаващо съобщение („3 нови съобщения“) вместо три последователни оповестявания. Матрицата по-горе показва, че всяка рамка се проваля на реда „серия“, така че решението трябва да е над рамката, а не в нея.

4

Тествайте с NVDA, JAWS и VoiceOver — и трите, всеки път

Матрицата не би съществувала, ако един екранен четец беше достатъчен. Строгостта на JAWS по отношение на празните региони и снизходителността на VoiceOver дърпат в противоположни посоки; NVDA е по средата. Модел, който се оповестява правилно само под VoiceOver — стойността по подразбиране за фронтенд екипите, работещи на Mac — е неработещ за по-голямата част от хората, които използват екранни четци.

5

Спрете да монтирате live региона условно

Най-честата грешка и в четирите рамки. Монтирайте региона празен при стартиране на приложението. Променяйте текста. Никога не го демонтирайте.


Заключение: aria-live е проблем на рамката, маскиран като проблем на маркирането

Прочитът на спецификацията ARIA на W3C оставя впечатлението, че aria-live е избор на маркиране — polite или assertive, с роля status, log или alert, и сте готови. Спецификацията е права в смисъл, че това са единствените регулатори, които тя признава. Спецификацията също е подвеждаща, защото предполага DOM, който мутира по начина, по който мутира императивен документ.

Всяка от посочените по-горе рамки въвежда планировчик между вашия код и DOM, а всеки планировчик има гранични случаи, които спецификацията не разглежда — автоматично групиране, прилагане на микрозадачи, дедупликация по време на компилация, графи на сигнали. Граничните случаи не са грешки в рамките; те са функционалности по замисъл, които просто взаимодействат зле с предположенията, които екранните четци правят за това кога настъпват мутациите на DOM.

Решението е структурно, а не за всеки компонент поотделно. Монтирайте глобални live региони при стартиране на приложението, насочвайте всяко оповестяване през малка услуга, ограничавайте пристъпните източници, тествайте на всичките три екранни четеца. Фактът, че един и същи петстъпков наръчник работи в React, Vue, Svelte и Solid, е най-силното доказателство, че избраната от вас рамка има по-малко значение от архитектурата, която изграждате около нея.

За по-широкия инструментариум на разработчика — модели за тестване, проверки по време на компилация и останалата част от картата на фронтенд достъпността — вижте страницата за разработчици; пълната справка с критериите за успех на WCAG 2.2 индексира критериите, които всеки от посочените по-горе модели засяга; безплатният скенер за WCAG 2.2 улавя структурните откази, които axe може да види на всеки URL адрес, който му посочите.

”The aria-live spec assumes the DOM mutates the way the spec wrote in 2008. Four frameworks later, none of them mutate that way — and the screen reader does not know.”

— инженерният отдел на Disability World, май 2026 г.