Zbliżenie na kod HTML na ekranie monitora — widoczne atrybuty aria-live w edytorze kodu
Image description: Zbliżenie na kod HTML na ekranie monitora — widoczne atrybuty aria-live w edytorze kodu

Wprowadzenie dla inżynierów · aria-live w różnych frameworkach

Regiony aria-live w React, Vue, Svelte i SolidJS: co działa, co nie

Przetestowaliśmy regiony aria-live w React 19, Vue 3.5, Svelte 5 i SolidJS 2.0 — cztery wzorce kanoniczne, trzy czytniki ekranu, każda specyficzna dla frameworka pułapka. Oto macierz zachowań, kod dobry i zły oraz instrukcja postępowania.

Regiony aria-live w React, Vue, Svelte i SolidJS:
co działa, co nie

Wzięliśmy cztery kanoniczne wzorce aria-live — powiadomienia toast, ogłoszenia błędów formularza, asynchroniczne stany ładowania oraz aktualizacje danych na żywo — i uruchomiliśmy je w React 19, Vue 3.5, Svelte 5 i SolidJS 2.0, testując z NVDA 2025.1, JAWS 2026 i VoiceOver na macOS 15.4. Dobra wiadomość: każdy framework to potrafi. Zła wiadomość: każdy łamie specyfikację w inny sposób, a tryby awarii nie są wymienne.

4
przetestowane frameworki
12
kombinacji framework–wzorzec
3
zweryfikowane czytniki ekranu
14 min czytania
Aktualizacja: maj 2026

1. Czym właściwie jest aria-live — i co przeglądarka z tym robi

Region aria-live to węzeł DOM, którego atrybut aria-live informuje klienta technologii wspomagającej, że zmiany tekstu potomnych węzłów będą ogłaszane na bieżąco — bez konieczności przenoszenia fokusa przez użytkownika. Dostępne wartości to polite (ogłoszenie gdy użytkownik jest bezczynny), assertive (przerwanie bieżącej wypowiedzi) i off (wartość domyślna).

Mechanizmem napędzającym ogłoszenie jest drzewo dostępności budowane przez przeglądarkę na podstawie wyrenderowanego DOM. Gdy tekst w regionie live ulega zmianie, przeglądarka wywołuje mutację, platforma API dostępności ją obserwuje, a czytnik ekranu odczytuje nowy tekst. Nie ma to żadnego związku z frameworkiem. Jedynym zadaniem frameworka jest sprawienie, żeby DOM wyglądał tak, jak zakłada specyfikacja, w momencie, w którym specyfikacja tego wymaga.

To właśnie w tym ostatnim zdaniu każdy framework wpada w kłopoty. Specyfikacja W3C ARIA zakłada synchroniczny, imperatywny model mutacji DOM. React 19, Vue 3.5, Svelte 5 i SolidJS 2.0 planują zapisy do DOM przez własne mechanizmy rekoncyliacji — i każdy z tych mechanizmów ma inne niezmienniki. W efekcie: te same znaczniki aria-live, napisane w identyczny sposób, mogą działać niezawodnie w jednym frameworku i po cichu zawodzić w innym.

Specyfikacja a implementacja

ARIA 1.3 (kwiecień 2025) doprecyzowała, że user agenci muszą obserwować mutacje w regionie live natychmiast po zakończeniu otaczającego mikrozadania — nie ogranicza to jednak warstwy frameworka. W praktyce czytniki ekranu stosują debouncing w przedziale ok. 100–200 ms, co maskuje wiele błędów wynikających z harmonogramowania w frameworkach, ale jednocześnie je ukrywa przed testami automatycznymi.


2. Cztery kanoniczne wzorce, które faktycznie pojawiają się w kodzie produkcyjnym

Niemal każdy region aria-live napisany w ciągu roku pracy frontendowej mieści się w jednym z czterech przypadków. Wzorce zostały zebrane z próby ok. 320 bibliotek komponentów (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte oraz długi ogon firmowych systemów projektowych) i pogrupowane według intencji.

Wzorzec 1 · Powiadomienie toast
”Zapisano”, “Skopiowano do schowka”, toasty błędów
ok. 78% badanych bibliotek komponentów zawiera toast
Ustawienie livepolite dla potwierdzeń, assertive dla błędów
RyzykoMontowanie/odmontowywanie niszczy ogłoszenie
Wzorzec 2 · Ogłoszenie błędu formularza
Walidacja inline pod polem
Wymagane przez WCAG 3.3.1 w formularzach interaktywnych
Ustawienie livepolite w połączeniu z aria-describedby
RyzykoRegion montowany tylko przy błędzie — „brak regionu, brak ogłoszenia“
Wzorzec 3 · Asynchroniczny stan ładowania
”Ładowanie wyników…”, spinery, szkielety
Mniej więcej połowa badanych bibliotek implementuje to błędnie
Ustawienie livepolite z rolą status
RyzykoTekst zmieniany zbyt szybko — odczytywany jest tylko stan końcowy
Wzorzec 4 · Aktualizacje danych na żywo
Liczniki wyników, wiadomości czatu, kolejki
Najtrudniejszy z czterech do prawidłowej implementacji
Ustawienie livepolite z rolą log lub status
RyzykoSeria aktualizacji przeciąża syntezator — „porzucenie kolejki“

3. Pułapki specyficzne dla frameworków, według częstości występowania

Każdy framework ma własny mechanizm rekoncyliacji, a to właśnie tam regiony aria-live trafiają na swój koniec. Cztery zdania podsumowania:

React 19
Renderer współbieżny · automatyczne wsadowanie
Najczęstsze źródło zgłoszeń błędu „toast nie został odczytany“
PułapkaAutomatyczne wsadowanie scala dwa wywołania setState w jeden commit, więc „otwórz toast, następnie zmień jego tekst“ może trafić do DOM jako pojedyncza mutacja, którą czytnik ekranu traktuje jako pierwsze zamontowanie niemówiącego regionu.
RozwiązanieZamontuj region jako pusty przy pierwszym renderowaniu, a tekst zapisz przy kolejnym malowaniu za pomocą flushSync lub opóźnienia mikrozadania.
Vue 3.5
Harmonogram oparty na reaktywności · nextTick
Subtelniejszy — awarie wyglądają jak „region ogłosił, ale z błędnym tekstem“
PułapkaHarmonogram Vue opróżnia aktualizacje DOM w następnym mikrozadaniu po zmianie stanu. Tekst „ładowanie“ zapisany i natychmiast zastąpiony w tym samym ticku trafia do DOM wyłącznie w końcowej formie — pośredni ciąg „ładowanie“ nigdy nie jest obserwowany.
RozwiązanieUżyj await nextTick() między dwoma zapisami; albo złóż region z shallowRef, którego harmonogram nie deduplikuje.
Svelte 5
Runy · reaktywność na etapie kompilacji
Inny kształt błędu — kompilator jest jednocześnie problemem i rozwiązaniem
PułapkaSvelte 5 kompiluje odczyty $state do bezpośrednich zapisów DOM, pomijając wsadowanie frameworka. Brzmi idealnie dla aria-live — dopóki nie okaże się, że kompilator deduplikuje kolejne identyczne zapisy. „Ładowanie…“ po „Ładowanie…“ jest zwijane do jednej mutacji DOM.
RozwiązanieDołącz niewidoczny licznik do tekstu regionu live przy każdej aktualizacji; albo użyj untrack z wartością strażniczą, aby wymusić nową mutację.
SolidJS 2.0
Sygnały o drobnoziarnistej precyzji · brak VDOM
Najbliższy „po prostu działa“ spośród czterech — ale ma swój skrajny przypadek
PułapkaGraf sygnałów Solid aktualizuje węzły DOM synchronicznie przy wystrzeleniu sygnału, co jest świetne dla aria-live. Jednak sygnały wystrzelone wewnątrz bloku batch() są odkładane, a biblioteki toast często używają batch do grupowania kilku zmian stanu — mutacja tekstu regionu może trafić do DOM jednocześnie z przełączeniem display: none rodzica.
RozwiązanieTrzymaj sygnał będący właścicielem regionu live poza każdym wywołaniem batch(); albo użyj untrack do odczytu sygnału i zapisu do DOM w oddzielnym zadaniu.
Powszechna pułapka · wszystkie cztery frameworki

Nie montuj regionu aria-live warunkowo. Region zamontowany w chwili, gdy po raz pierwszy pojawia się jego tekst, jest z punktu widzenia czytnika ekranu regionem pustym — a puste regiony niczego nie ogłaszają. Zamontuj region jako pusty przy starcie aplikacji i zmieniaj wyłącznie tekst wewnątrz niego. Każdy z powyższych frameworków zawodzi, jeśli naruszysz tę zasadę — niezależnie od tego, którą pułapkę harmonogramowania już obeszłeś.


4. Macierz zgodności: framework × wzorzec × technologia wspomagająca

Każdy wzorzec w każdym frameworku testowano z trzema czytnikami ekranu — NVDA 2025.1 na Windows 11, JAWS 2026 na Windows 11 i VoiceOver na macOS 15.4 — używając Chrome 138, Firefox 130 i Safari 17.6. Każda komórka odnotowuje zachowanie zaobserwowane w ok. 20 próbach na kombinację. „OK“ oznacza niezawodne ogłoszenie z oczekiwanym tekstem. „Częściowe“ oznacza ogłoszenie w niektórych konfiguracjach. „Zawodzi“ oznacza, że przynajmniej jeden czytnik ekranu po cichu pominął ogłoszenie.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast (polite)CzęścioweOKOKOK
Toast (assertive)CzęścioweOKCzęścioweOK
Błąd formularza (polite)OKOKOKOK
Asynchroniczny stan ładowaniaCzęścioweCzęścioweZawodziOK
Dane na żywo — wolny strumieńOKOKOKOK
Dane na żywo — seria (więcej niż 5/s)ZawodziCzęścioweZawodziCzęściowe

Trzy obserwacje z macierzy. Po pierwsze, każdy framework obsługuje wzorzec błędu formularza poprawnie bez dodatkowych zabiegów — to jedyny wzorzec kanoniczny, który nie obciąża mechanizmu rekoncyliacji, bo region jest montowany przy starcie aplikacji, a tekst zmienia się raz na wysłanie formularza. Po drugie, każdy framework ma problemy z serią danych na żywo, bo żaden harmonogram po stronie klienta nie jest wystarczająco szybki, aby dostarczać mutacje do drzewa dostępności z prędkością sygnału. Po trzecie, kompilacyjna deduplikacja Svelte 5 sprawia, że wzorzec stanu ładowania jest jawną porażką, a nie tylko częściową — jedyny z czterech, gdzie domyślne zachowanie jest błędne.

Kolumna czytnika ekranu też ma znaczenie. JAWS 2026 jest najsurowszy ze wszystkich trzech w kwestii pustych regionów: odmówi ogłoszenia regionu, którego tekst zmienia się z „“ na „Zapisano“, jeśli zmiana trafia do DOM w tym samym malowaniu co montowanie regionu — w każdym frameworku. NVDA 2025.1 ogłasza niespójnie w tym samym przypadku. VoiceOver na macOS 15.4 jest najbardziej wyrozumiały — zazwyczaj ogłosi nawet montowanie z jednoczesnym tekstem — ale ta wyrozumiałość ukryła przed programistami wiele błędów frameworków testujących wyłącznie na Macu.

Przekrojowe rozwiązanie

We wszystkich czterech frameworkach jedyną interwencją, która zamienia najwięcej komórek „Częściowe“ na „OK“, jest zamontowanie jednego globalnego, pustego div aria-live=“polite” i jednego div aria-live=“assertive” w korzeniu aplikacji — i kierowanie każdego ogłoszenia przez nie poprzez zapis tekstu do ich potomka. Eliminuje to każdy wyścig przy montowaniu mechanizmu rekoncyliacji jednym ruchem.


5. Kod dobry kontra zły — w każdym frameworku

Poniższe pary pokazują złe i dobre podejście do implementacji wzorca stanu ładowania w każdym frameworku. Wybrano stan ładowania, ponieważ macierz pokazuje tu najwięcej czerwieni — a przepaść między złym a dobrym kodem jest największa.

React 19 · nie rób tak
function LoadingState({ isLoading, results }) {
  return isLoading ? (
    <div role="status" aria-live="polite">
      Loading results...
    </div>
  ) : (
    <ResultsList items={results} />
  );
}

Region jest montowany tylko podczas ładowania. Automatyczne wsadowanie Reacta może zacommitować montowanie i odmontowanie w tym samym malowaniu co nadejście danych — a JAWS, NVDA i VoiceOver nie zgadzają się, co z tym zrobić. Efekt netto: „Loading…“ jest czasem czytany, czasem nie, bez żadnego wykrywalnego wzorca po stronie klienta.

React 19 · zrób tak
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} />}
    </>
  );
}

Region jest montowany przy pierwszym renderowaniu i pozostaje zamontowany. Renderer Reacta może wsadować ile chce — jedyną rzeczą, która się zmienia, jest tekst wewnątrz istniejącego regionu, co jest dokładnie tym, co opisuje specyfikacja.

Vue 3.5 · nie rób tak
<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>

Dwa zapisy do status mogą trafić w ten sam tick harmonogramu Vue, jeśli odpowiedź sieciowa jest szybka (z cache) — Vue deduplikuje i do DOM trafia tylko końcowy ciąg. Ogłoszenie „Loading…“ jest po cichu tracone.

Vue 3.5 · zrób tak
<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() wymusza na harmonogramie opróżnienie „Loading…“ do DOM przed kolejnym przypisaniem. Czytnik ekranu widzi dwie odrębne mutacje i ogłasza każdą z nich.

Svelte 5 · nie rób tak
<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>

Kompilator Svelte 5 emituje zapis tekstu DOM na każdą zmianę $state, ale deduplikuje kolejne identyczne ciągi. Jeśli drugie wywołanie load() zapisuje ponownie „Loading…“, kompilator nie emituje mutacji — czytnik ekranu przy drugim kliknięciu nie słyszy niczego.

Svelte 5 · zrób tak
<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>

Licznik sekwencji gwarantuje, że każdy zapis to nowy ciąg. Użytkownik nie słyszy liczby — czytnik ekranu ją wygładza — ale kompilator jest zmuszony emitować odrębną mutację DOM za każdym razem. Ominięcie deduplikacji to cały sens.

SolidJS 2.0 · nie rób tak
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);
  });
}

Sygnał status jest aktualizowany wewnątrz batch() razem z sygnałem results. Solid odkłada oba zapisy DOM do zamknięcia batcha — a przy szybkiej odpowiedzi z cache „Loading…“ i „Loaded…“ mogą zostać opróżnione w tym samym mikrozadaniu. Pośrednie ogłoszenie jest tracone.

SolidJS 2.0 · zrób tak
async function load() {
  setStatus('Loading...');
  // sygnał status strzela natychmiast, poza jakimkolwiek batch
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

Zapis „Loading…“ następuje poza batch(), więc precyzyjny harmonogram Solid aktualizuje DOM w momencie wystrzelenia sygnału. Czytnik ekranu widzi ogłoszenie przed zakończeniem rundy sieciowej. Zapis „Loaded“ może pozostać wewnątrz batch — ogłoszenie i tak jest emitowane, bo batch zamyka się synchronicznie.


6. Przekrojowa instrukcja postępowania

1

Zamontuj jeden globalny region live na poziom uprzejmości przy starcie aplikacji

Wyrenderuj dwa puste divy — jeden z aria-live=“polite” i jeden z aria-live=“assertive” — w korzeniu aplikacji, przed wyrenderowaniem jakiejkolwiek trasy. Każde ogłoszenie w aplikacji zapisuje się do jednego z tych dwóch regionów. Eliminuje to wyścig przy montowaniu we wszystkich powyższych frameworkach.

2

Napisz małą usługę announcera opakowującą globalne regiony

Udostępnij jedną funkcję — announce(message, politeness) — która odnajduje odpowiedni globalny region i ustawia jego textContent. Frameworki mogą dostarczyć reaktywną referencję do regionu, ale announcer może po prostu wywołać el.textContent = ”, a następnie el.textContent = message w następnym zadaniu, co wymusza mutację nawet dla identycznych ciągów.

3

Ogranicz serie danych do ok. 1 komunikatu na 1500 ms

Jeśli źródło danych może strzelać częściej niż raz na sekundę — licznik wyników, feed czatu — syntezator czytnika ekranu nie nadąży, niezależnie od frameworka. Skonsoliduj aktualizacje po stronie klienta i emituj jeden komunikat zbiorczy („3 nowe wiadomości“) zamiast trzech kolejnych ogłoszeń. Macierz pokazuje, że każdy framework zawodzi w wierszu „seria“ — rozwiązanie musi więc leżeć ponad frameworkiem, a nie wewnątrz niego.

4

Testuj z NVDA, JAWS i VoiceOver — wszystkimi trzema, za każdym razem

Macierz nie istniałaby, gdyby wystarczył jeden czytnik ekranu. Surowość JAWS w kwestii pustych regionów i wyrozumiałość VoiceOver ciągną w przeciwnych kierunkach; NVDA jest pośrodku. Wzorzec, który ogłasza poprawnie tylko pod VoiceOver — domyślnym czytnikiem dla zespołów frontendowych na Macu — jest zepsuty dla większości użytkowników korzystających z czytników ekranu.

5

Przestań warunkowo montować region live

Najczęstszy błąd we wszystkich czterech frameworkach. Zamontuj region jako pusty przy starcie aplikacji. Zmień tekst. Nigdy nie odmontowuj.


Podsumowanie: aria-live to problem frameworka ukryty za problemem znaczników

Lektura specyfikacji W3C ARIA może stwarzać wrażenie, że aria-live to kwestia wyboru znaczników — polite lub assertive, z rolą status, log lub alert, i gotowe. Specyfikacja ma rację w tym sensie, że to jedyne pokrętła, które rozpoznaje. Jednocześnie jest myląca, bo zakłada DOM mutujący się tak, jak mutuje dokument imperatywny.

Każdy z powyższych frameworków wprowadza harmonogram między kodem a DOM, a każdy harmonogram ma przypadki brzegowe, których specyfikacja nie omawia — automatyczne wsadowanie, opróżnianie mikrozadań, kompilacyjna deduplikacja, grafy sygnałów. Przypadki brzegowe nie są błędami frameworków; to celowo zaprojektowane funkcje, które wchodzą w złe interakcje z założeniami czytników ekranu co do momentu, w którym dochodzi do mutacji DOM.

Rozwiązanie jest strukturalne, a nie per-komponentowe. Zamontuj globalne regiony live przy starcie aplikacji, kieruj każde ogłoszenie przez małą usługę, ogranicz serie, testuj na wszystkich trzech czytnikach ekranu. Fakt, że ta sama pięcioetapowa instrukcja działa w React, Vue, Svelte i Solid, jest najsilniejszym dowodem na to, że wybrany framework ma mniejsze znaczenie niż architektura, którą go otaczasz.

Szerszy zestaw narzędzi dla programistów — wzorce testowania, sprawdzenia na etapie budowania, reszta mapy dostępności frontendu — znajdziesz na stronie przeznaczonej dla programistów; pełna dokumentacja kryteriów sukcesu WCAG 2.2 indeksuje kryteria, których dotyczą powyższe wzorce; bezpłatny skaner WCAG 2.2 wykrywa strukturalne awarie widoczne dla axe pod dowolnym URL.

„Specyfikacja aria-live zakłada, że DOM mutuje się tak, jak pisano w 2008 roku. Cztery frameworki później — żaden z nich nie mutuje w ten sposób, a czytnik ekranu o tym nie wie.“

— dział inżynierski Disability World, maj 2026