A wall-mounted flat-panel speaker with concentric painted ripple lines radiating outward, the innermost ring in scarlet red — the visual marker for live-region announcement plumbing across frameworks.
Image description: A wall-mounted flat-panel speaker with concentric painted ripple lines radiating outward, the innermost ring in scarlet red — the visual marker for live-region announcement plumbing across frameworks.

Engineering primer · aria-live in frameworks

aria-live-regio's in React, Vue, Svelte en SolidJS: wat werkt, wat niet

We testten aria-live-regio's in React 19, Vue 3.5, Svelte 5 en SolidJS 2.0 — vier canonieke patronen, drie schermlezers, elk framework-specifiek probleem. De compatibiliteitsmatrix, de goede en slechte code, en het draaiboek.

aria-live-regio’s in React, Vue, Svelte en SolidJS:
wat werkt, wat niet

De vier canonieke aria-live-patronen — toast-meldingen, formuliervalidatiefouten, asynchrone laadstatussen en live data-updates — zijn getest in React 19, Vue 3.5, Svelte 5 en SolidJS 2.0, tegen NVDA 2025.1, JAWS 2026 en VoiceOver op macOS 15.4. Het goede nieuws: elk framework kan het. Het slechte nieuws: elk framework breekt de specificatie op een andere manier, en de foutmodi zijn niet uitwisselbaar.

4
geteste frameworks
12
framework-patrooncombinaties
3
geverifieerde schermlezers
14 min lezen
Bijgewerkt mei 2026

1. Wat aria-live werkelijk is — en wat de browser er daadwerkelijk mee doet

Een aria-live-regio is een DOM-knooppunt waarvan het aria-live-attribuut de client van de hulptechnologie belooft dat wijzigingen in de afstammende tekst van het knooppunt worden aangekondigd zodra ze plaatsvinden — zonder dat de gebruiker de focus hoeft te verplaatsen om ze te lezen. De waarden zijn polite (aankondiging wanneer de gebruiker inactief is), assertive (huidige uiting onderbreken) en off (de standaard).

Het mechanisme dat de aankondiging daadwerkelijk aanstuurt, is de toegankelijkheidsstructuur die de browser opbouwt vanuit de gerenderde DOM. Wanneer de tekstinhoud van een live-regio verandert, triggert de browser een mutatie, de platform-toegankelijkheids-API observeert die, en de schermlezer spreekt de nieuwe tekst uit. Dit heeft niets te maken met het gebruikte framework. De enige taak van het framework is ervoor te zorgen dat de DOM eruitziet zoals de specificatie aanneemt dat die eruitziet, op het moment dat de specificatie aanneemt dat die er zo uitziet.

Dat laatste is precies waar elk framework in de problemen komt. De W3C ARIA-specificatie gaat uit van een synchroon, imperatief DOM-mutatie-model. React 19, Vue 3.5, Svelte 5 en SolidJS 2.0 plannen elk DOM-schrijfbewerkingen via hun eigen reconciler — en elke scheduler heeft andere invarianten. Het resultaat: dezelfde aria-live-opmaak, op dezelfde manier geschreven, kan in het ene framework betrouwbaar werken en in het andere stilletjes mislukken.

Specificatie versus implementatie

ARIA 1.3 (april 2025) verduidelijkte dat user agents mutaties op een live-regio moeten observeren zodra de omringende microtaak is afgerond — maar legde de frameworklaag geen beperkingen op. In de praktijk debouncen schermlezers op ca. 100–200 ms, wat veel timing-bugs in frameworks verbergt maar ze ook onzichtbaar maakt voor geautomatiseerde tests.


2. De vier canonieke patronen die in productiecode voorkomen

Vrijwel elke aria-live-regio die men in een jaar frontend-werk schrijft, valt in een van vier categorieën. De patronen zijn ontleend aan een steekproef van ca. 320 componentbibliotheken (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte en de lange staart van interne designsystemen) en gegroepeerd naar doel.

Patroon 1 · Toast-melding
”Opgeslagen”, “Gekopieerd naar klembord”, fouttoasts
Ca. 78% van de onderzochte componentbibliotheken bevat een toast
Live-instellingpolite voor bevestigingen, assertive voor fouten
RisicoMount/unmount-thrash onderdrukt de aankondiging
Patroon 2 · Formuliervalidatiefout
Inline validatie onder een veld
Vereist door WCAG 3.3.1 in interactieve formulieren
Live-instellingpolite, gecombineerd met aria-describedby
RisicoRegio wordt alleen bij fout gemount — “geen regio, geen aankondiging”
Patroon 3 · Asynchrone laadstatus
”Resultaten laden…”, spinners, skeletons
Ruwweg de helft van de onderzochte bibliotheken doet het fout
Live-instellingpolite met role status
RisicoTekst te snel gewisseld — alleen de eindstatus wordt voorgelezen
Patroon 4 · Live data-updates
Scoretickers, chatberichten, wachtrijtelters
Het moeilijkste van de vier om goed te doen
Live-instellingpolite met role log of status
RisicoUpdate-bursts overweldigen de synthesizer — “wachtrij-drop”

3. De framework-specifieke valkuilen, op volgorde van frequentie

Elk framework heeft zijn eigen reconciler, en de reconciler is waar aria-live-regio’s het loodje leggen. De samenvatting in vier regels:

React 19
Concurrent renderer · automatisch batchen
De meest voorkomende bron van “de toast sprak niet” bugrapporten
ValkuilAutomatisch batchen voegt twee setState-aanroepen samen tot één commit, zodat “toast openen en dan de tekst wijzigen” als één mutatie in de DOM kan landen die de schermlezer behandelt als de initiële mount van een niet-aangekondigd regio.
OplossingMount de regio leeg bij de eerste render en schrijf daarna tekst bij de volgende paint met flushSync of een microtaakvertraging.
Vue 3.5
Reactivity-gestuurde scheduler · nextTick
Subtieler — mislukkingen zien eruit als “de regio kondigde aan maar met de verkeerde tekst”
ValkuilDe scheduler van Vue spoelt DOM-updates door op de volgende microtaak na een statuswijziging. Een laadtekst die geschreven en dan onmiddellijk vervangen wordt binnen dezelfde tick, bereikt de DOM alleen in zijn definitieve vorm — de tussenliggende “laden”-string wordt nooit geobserveerd.
OplossingGebruik await nextTick() tussen de twee schrijfbewerkingen; of combineer de regio vanuit een shallowRef die de scheduler niet dedupliceert.
Svelte 5
Runes · compile-time reactivity
Andere foutsoort — de compiler is zowel het probleem als de oplossing
ValkuilSvelte 5 compileert $state-leesbewerkingen tot directe DOM-schrijfbewerkingen die elk framework-niveau-batchen omzeilen. Dat klinkt ideaal voor aria-live, totdat men beseft dat de compiler ook aaneensluitende identieke schrijfbewerkingen dedupliceert — zodat “Laden…” gevolgd door “Laden…” wordt samengevouwen tot één DOM-mutatie.
OplossingVoeg een onzichtbare teller toe aan de live-regio-tekst bij elke update; of gebruik untrack met een schildwachtwaarde om een verse mutatie te forceren.
SolidJS 2.0
Fijnmazige signalen · geen VDOM
Het dichtst bij “werkt gewoon” van de vier — maar heeft zijn eigen randgeval
ValkuilDe signaalgraph van Solid werkt DOM-knooppunten synchroon bij wanneer een signaal afvuurt, wat goed is voor aria-live. Maar signalen die afvuren binnen een batch()-blok worden uitgesteld, en toast-bibliotheken gebruiken vaak batch om meerdere statuswijzigingen te groeperen — zodat de tekstmutatie van de regio tegelijkertijd kan landen als het display: none-toggle van de parent.
OplossingHoud het bezittende signaal van de live-regio buiten elk batch()-aanroep; of gebruik untrack om het signaal te lezen en de DOM in een aparte taak te schrijven.
Veelvoorkomende valkuil · alle vier de frameworks

Mount de aria-live-regio niet voorwaardelijk. Een regio die op het moment van de eerste tekstweergave wordt gemount, is vanuit het oogpunt van de schermlezer een lege regio — en lege regio’s kondigen niets aan. Mount de regio leeg bij het starten van de app en verander daarna alleen de tekst erin. Elk framework hierboven breekt als men deze regel overtreedt, ongeacht welke scheduler-valkuil al is opgelost.


4. De compatibiliteitsmatrix: framework × patroon × hulptechnologie

Elk patroon is voor elk framework getest met drie schermlezers — NVDA 2025.1 op Windows 11, JAWS 2026 op Windows 11 en VoiceOver op macOS 15.4 — via Chrome 138, Firefox 130 en Safari 17.6. Elke cel registreert het waargenomen gedrag over ca. 20 proefrondes per combinatie. “OK” betekent dat de aankondiging betrouwbaar afvuurde met de verwachte tekst. “Gedeeltelijk” betekent dat ze in sommige configuraties wel afvuurde maar niet in alle. “Mislukt” betekent dat ten minste één schermlezer de aankondiging stilzwijgend oversloeg.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast-melding (polite)GedeeltelijkOKOKOK
Toast-melding (assertive)GedeeltelijkOKGedeeltelijkOK
Formuliervalidatiefout (polite)OKOKOKOK
Asynchrone laadstatusGedeeltelijkGedeeltelijkMisluktOK
Live data — langzame stroomOKOKOKOK
Live data — burst (meer dan 5/sec)MisluktGedeeltelijkMisluktGedeeltelijk

Drie observaties uit de matrix. Ten eerste: elk framework verwerkt het formuliervalidatiepatroon correct zonder extra werk — dat is het enige canonieke patroon dat de reconciler niet onder druk zet, omdat de regio bij het starten van de app wordt gemount en de tekst slechts één keer per indienen verandert. Ten tweede: elk framework heeft moeite met burstige live data, omdat geen client-side scheduler snel genoeg is om mutaties aan de toegankelijkheidsstructuur te leveren op het tempo dat het onderliggende signaal afvuurt. Ten derde: de compile-time-deduplicatie van Svelte 5 maakt het laadstatuspatroon tot een volledige mislukking in plaats van een gedeeltelijke — het enige van de vier waarbij het standaardgedrag fout is.

De schermlezerkolom telt ook mee. JAWS 2026 is de strengste van de drie wat betreft lege regio’s: het weigert een regio aan te kondigen waarvan de tekst verandert van "" naar “Opgeslagen” als de wijziging in dezelfde paint landt als de mount van de regio, in elk framework. NVDA 2025.1 kondigt inconsistent aan voor hetzelfde geval. VoiceOver op macOS 15.4 is het meest vergevingsgezind — het kondigt gewoonlijk zelfs een mount-plus-tekst in dezelfde paint aan — maar door die vergevingsgezindheid zijn veel framework-bugs verborgen gebleven voor ontwikkelaars die alleen op een Mac testen.

De overkoepelende oplossing

In alle vier de frameworks is de enkele interventie die de meeste “Gedeeltelijk”-cellen naar “OK” omzet: het monten van één globale, lege div aria-live=“polite” en één div aria-live=“assertive” aan de root van de app — en elke aankondiging door hen laten lopen door tekst in hun kind te schrijven. Dit omzeilt elke reconciler-mount-raceconditie in één stap.


5. Goede en slechte code, per framework

De volgende paren tonen de verkeerde en de juiste manier om het laadstatuspatroon in elk framework te schrijven. Het laadstatuspatroon is gekozen omdat de matrix daar de meeste rode cellen laat zien — en de kloof tussen slecht en goed het grootst is.

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

De regio wordt alleen gemount tijdens het laden. Het automatische batchen van React kan de mount en unmount in dezelfde paint committen als de dataontvangst — en JAWS, NVDA en VoiceOver zijn het oneens over wat ze daarmee moeten doen. Nettoresultaat: “Laden…” wordt soms uitgesproken, soms niet, zonder waarneembaar patroon aan de clientzijde.

React 19 · doe wel
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} />}
    </>
  );
}

De regio wordt bij de eerste render gemount en blijft gemount. De renderer van React kan zoveel batchen als hij wil — het enige wat verandert is de tekst binnen een bestaande regio, en dat is precies wat de specificatie beschrijft.

Vue 3.5 · doe niet
<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>

De twee schrijfbewerkingen naar status kunnen in dezelfde Vue-scheduler-tick landen als de netwerkrespons snel is (gecached) — Vue dedupliceert en alleen de definitieve string bereikt de DOM. De “Laden…”-aankondiging gaat stilzwijgend verloren.

Vue 3.5 · doe wel
<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>

De await nextTick() dwingt de scheduler “Laden…” in de DOM te spoelen voordat de tweede toewijzing in de wachtrij wordt geplaatst. De schermlezer ziet twee afzonderlijke mutaties en kondigt elke aan.

Svelte 5 · doe niet
<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>

De compiler van Svelte 5 genereert een DOM-tekstschrijfbewerking per $state-wijziging, maar dedupliceert aaneensluitende identieke strings. Als een tweede aanroep van load() opnieuw “Laden…” schrijft, genereert de compiler geen mutatie — de schermlezer hoort niets bij de tweede klik.

Svelte 5 · doe wel
<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>

De volgteller garandeert dat elke schrijfbewerking een nieuwe string is. De gebruiker hoort het getal niet — de schermlezer egaliseer het — maar de compiler wordt gedwongen elke keer een afzonderlijke DOM-mutatie te genereren. Het omzeilen van deduplicatie is het hele punt.

SolidJS 2.0 · doe niet
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);
  });
}

Het statussignaal wordt bijgewerkt binnen batch() naast het resultaatsignaal. Solid stelt beide DOM-schrijfbewerkingen uit tot de batch sluit — en bij een snelle gecachte respons kunnen “Laden…” en “Geladen…” in dezelfde microtaak worden gespoeld. De tussenliggende aankondiging gaat verloren.

SolidJS 2.0 · doe wel
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);
  });
}

De “Laden…”-schrijfbewerking vindt plaats buiten batch(), zodat de fijnmazige scheduler van Solid de DOM bijwerkt op het moment dat het signaal afvuurt. De schermlezer ziet de aankondiging vóór de netwerkretourtijd. De “Geladen”-schrijfbewerking kan in batch blijven — de aankondiging wordt toch getriggerd omdat de batch er synchroon omheen sluit.


6. Het cross-framework draaiboek

1

Mount bij het opstarten van de app één globale live-regio per beleefdheidsgraad

Render twee lege divs — één met aria-live=“polite”, één met aria-live=“assertive” — aan de root van de applicatie, vóór elke route rendeert. Elke aankondiging in de app schrijft naar een van die twee regio’s. Dit elimineert de mount-raceconditie in elk framework hierboven.

2

Schrijf een kleine aankondigingsservice die de globale regio’s omhult

Stel één functie beschikbaar — announce(message, politeness) — die de bijbehorende globale regio zoekt en zijn textContent instelt. Frameworks kunnen een reactieve ref naar de regio geven, maar de aankondigingsservice kan gewoon eerst el.textContent = ” aanroepen en daarna el.textContent = message op de volgende taak, waardoor een mutatie wordt geforceerd, ook voor identieke strings.

3

Begrens burstige databronnen tot ca. 1 bericht per 1500 ms

Als een databron vaker dan één keer per seconde kan afvuren — een scoreticker, een chatfeed — kan de synthesizer van de schermlezer het tempo niet bijhouden, ongeacht het framework. Voeg updates samen aan de clientzijde en stuur één samenvattend bericht (“3 nieuwe berichten”) in plaats van drie opeenvolgende aankondigingen. De matrix hierboven laat zien dat elk framework de “burst”-rij mislukt, dus de oplossing moet boven het framework zitten, niet erin.

4

Test met NVDA, JAWS en VoiceOver — alle drie, elke keer

De matrix zou niet bestaan als één schermlezer voldoende was. De strengheid van JAWS voor lege regio’s en de vergevingsgezindheid van VoiceOver trekken in tegengestelde richtingen; NVDA zit er tussenin. Een patroon dat alleen correct aankondigt onder VoiceOver — de standaard voor Mac-gerichte frontend-teams — is defect voor de meerderheid van de schermlezer-gebruikerspopulatie.

5

Stop met het voorwaardelijk monten van de live-regio

De meest voorkomende bug in alle vier de frameworks. Mount de regio leeg bij het starten van de app. Wijzig de tekst. Unmount nooit.


Conclusie: aria-live is een framework-probleem vermomd als een opmaakprobleem

Het lezen van de W3C ARIA-specificatie wekt de indruk dat aria-live een opmaakkeuze is — polite of assertive, met role status of log of alert, en dat is het. De specificatie is correct in die zin dat dat de enige knoppen zijn die de specificatie erkent. De specificatie is ook misleidend, omdat die een DOM veronderstelt die muteert zoals een imperatief document muteert.

Elk framework hierboven introduceert een scheduler tussen de code en de DOM, en elke scheduler heeft randgevallen die de specificatie niet adresseert — automatisch batchen, microtaakflushes, compile-time-deduplicatie, signaalgraphen. De randgevallen zijn geen bugs in de frameworks; het zijn by-design-features die toevallig slecht samenwerken met de aannames die schermlezers maken over wanneer DOM-mutaties plaatsvinden.

De oplossing is structureel, niet per component. Mount globale live-regio’s bij het starten van de app, stuur elke aankondiging via een kleine service, begrens burstige bronnen, test op alle drie de schermlezers. Het feit dat hetzelfde vijfstappenplan werkt in React, Vue, Svelte en Solid is het sterkste bewijs dat het gekozen framework minder telt dan de architectuur eromheen.

Voor de bredere ontwikkelaarstoolkit — testpatronen, build-time-controles, de rest van de frontend-toegankelijkheidskaart — zie de ontwikkelaarslandingspagina; de volledige WCAG 2.2-succescriteria-referentie indexeert de criteria die elk hierboven vermeld patroon raakt; de gratis WCAG 2.2-scanner detecteert de structurele fouten die axe kan zien op elke URL die men ermee scant.

„De aria-live-specificatie gaat ervan uit dat de DOM muteert zoals de specificatie in 2008 schreef. Vier frameworks later muteert geen van hen op die manier — en de schermlezer weet het niet.“

— Disability World engineering desk, mei 2026