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 på tværs af frameworks

aria-live-regioner i React, Vue, Svelte og SolidJS: hvad der virker, hvad der ikke gør

Vi testede aria-live-regioner på tværs af React 19, Vue 3.5, Svelte 5 og SolidJS 2.0 — fire kanoniske mønstre, tre skærmlæsere, alle framework-specifikke faldgruber. Her er adfærdsmatricen, god-vs-dårlig kode og spillebogen.

aria-live-regioner i React, Vue, Svelte og SolidJS:
hvad der virker, hvad der ikke gør

Vi tog de fire kanoniske aria-live-mønstre — toast-notifikationer, formulafeltsmeddelelser, asynkrone indlæsningstilstande og live dataopdateringer — og kørte dem gennem React 19, Vue 3.5, Svelte 5 og SolidJS 2.0 mod NVDA 2025.1, JAWS 2026 og VoiceOver på macOS 15.4. Den gode nyhed: alle frameworks kan gøre det. Den dårlige nyhed: hvert af dem bryder specifikationen på sin egen måde, og fejlmønstrene er ikke overførbare.

4
frameworks testet
12
framework-mønster-kombinationer
3
skærmlæsere verificeret
14 min. læsning
Opdateret maj 2026

1. Hvad aria-live faktisk er — og hvad browseren faktisk gør med det

En aria-live-region er en DOM-knude, hvis aria-live-attribut lover hjælpeteknologiklienten, at ændringer i knudens descendant-tekst vil blive annonceret, efterhånden som de sker — uden at brugeren behøver at flytte fokus for at læse dem. Værdierne er polite (annoncér, når brugeren er inaktiv), assertive (afbryd den aktuelle ytring) og off (standard).

Den mekanisme, der faktisk driver annonceringen, er tilgængelighed-træet, som browseren bygger fra den renderede DOM. Når tekstindholdet i en live-region ændres, fyrer browseren en mutation, platform-tilgængeligheds-API’en observerer den, og skærmlæseren taler den nye tekst. Intet af dette har noget at gøre med dit framework. Frameworkets eneste opgave er at få DOM til at se ud, som specifikationen forudsætter, at den vil se ud, på det tidspunkt specifikationen forudsætter, at den vil gøre det.

Den sidste sætning er, hvor alle frameworks kommer i problemer. W3C ARIA-specifikationen antager en synkron, imperativ DOM-mutationsmodel. React 19, Vue 3.5, Svelte 5 og SolidJS 2.0 planlægger alle DOM-skrivninger via deres egne reconcilere — og hver reconcilers scheduler har forskellige invarianter. Resultatet: den samme aria-live-markup, skrevet på samme måde, kan fyre pålideligt i ét framework og stille fejle i et andet.

Specifikation versus implementering

ARIA 1.3 (april 2025) præciserede, at brugeragenter skal observere mutationer på en live-region, så snart den omgivende mikrotask er færdig — men den begrænsede ikke framework-laget. I praksis debouncer skærmlæsere ved ca. 100–200 ms, hvilket dækker over mange framework-timing-fejl men også skjuler dem for automatiserede tests.


2. De fire kanoniske mønstre, der faktisk optræder i produktionskode

Næsten alle aria-live-regioner, du vil skrive i et år med frontend-arbejde, falder i en af fire spande. Vi hentede mønstrene fra et udvalg af ca. 320 komponentbiblioteker (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte og den lange hale af in-house designsystemer) og grupperede dem efter hensigt.

Mønster 1 · Toast-notifikation
”Gemt”, “Kopieret til udklipsholder”, fejl-toasts
ca. 78% af de undersøgte komponentbiblioteker leverer en toast
Live-indstillingpolite til bekræftelser, assertive til fejl
RisikoMount/unmount-thrash dræber annonceringen
Mønster 2 · Formulafeltsmeddelelse
Inline-validering under et felt
Krævet af WCAG 3.3.1 i interaktive formularer
Live-indstillingpolite, parret med aria-describedby
RisikoRegion monteres kun ved fejl — »ingen region, ingen annoncering«
Mønster 3 · Asynkron indlæsningstilstand
”Indlæser resultater…”, spinnere, skeletons
Omtrent halvdelen af de undersøgte biblioteker gør det forkert
Live-indstillingpolite med role status
RisikoTekst skiftet for hurtigt — kun den endelige tilstand læses op
Mønster 4 · Live dataopdateringer
Scoretickere, chatbeskeder, kø-tællere
Det sværeste af de fire at gøre rigtigt
Live-indstillingpolite med role log eller status
RisikoOpdateringsudbrud overbelaster synthesizeren — »kø-drop«

3. De framework-specifikke faldgruber, i hyppighedsrækkefølge

Hvert framework har sin egen reconciler, og reconcileren er, hvor aria-live-regioner går til at dø. Firelinjeopsummeringen:

React 19
Concurrent renderer · automatisk batching
Den hyppigste kilde til fejlrapporter af typen »toasten talte ikke«
FaldgrubeAutomatisk batching samler to setState-kald i en enkelt commit, så »åbn toast og skift derefter dens tekst« kan lande i DOM som en enkelt mutation, som skærmlæseren behandler som den indledende mount af en region, der ikke er talt.
LøsningMount regionen tom ved første rendering, skriv derefter tekst ved næste paint med flushSync eller en mikrotask-forsinkelse.
Vue 3.5
Reaktivitetsdrevet scheduler · nextTick
Mere subtil — fejlene ligner »regionen annoncerede men med den forkerte tekst«
FaldgrubeVues scheduler skyller DOM-opdateringer på den næste mikrotask efter en tilstandsændring. En indlæsningstekst, der skrives og straks erstattes inde i samme tick, når kun DOM i sin endelige form — den mellemliggende »indlæser«-streng observeres aldrig.
LøsningBrug await nextTick() mellem de to skrivninger; eller sammensæt regionen fra en shallowRef, som scheduleren ikke deduplikerer.
Svelte 5
Runes · compile-time reaktivitet
Anden fejlform — compileren er både problemet og løsningen
FaldgrubeSvelte 5 kompilerer $state-læsninger til direkte DOM-skrivninger, der omgår al framework-niveau batching. Det lyder ideelt til aria-live, indtil man indser, at compileren også deduplikerer på hinanden følgende identiske skrivninger — så »Indlæser…« efterfulgt af »Indlæser…« sammenfalder til én DOM-mutation.
LøsningTilføj en usynlig tæller til live-regionsteksten ved hver opdatering; eller brug untrack med en sentinel-værdi for at tvinge en ny mutation.
SolidJS 2.0
Fine-grained signals · ingen VDOM
Tættest på »bare virker« af de fire — men har sin egen grænsetilfælde
FaldgrubeSolids signal-graf opdaterer DOM-knuder synkront, når et signal fyrer, hvilket er godt til aria-live. Men signaler der fyrer inde i en batch()-blok udskydes, og toast-biblioteker bruger ofte batch til at gruppere flere tilstandsændringer — så regionens tekstmutation kan lande samtidig med forælderens display: none-skift.
LøsningHold live-regionens ejende signal uden for ethvert batch()-kald; eller brug untrack til at læse signalet og skrive DOM i en separat opgave.
Fælles faldgrube · alle fire frameworks

Undlad at montere aria-live-regionen betinget. En region, der monteres i det øjeblik dens tekst første gang vises, er — fra skærmlæserens perspektiv — en tom region — og tomme regioner annoncerer ingenting. Mount regionen tom ved app-start, og skift kun teksten inde i den. Alle frameworks ovenfor bryder, hvis du overtræder denne regel, uanset hvilken scheduler-faldgrube du allerede har omgået.


4. Kompatibilitetsmatricen: framework × mønster × hjælpeteknologi

Vi kørte hvert mønster under hvert framework mod tre skærmlæsere — NVDA 2025.1 på Windows 11, JAWS 2026 på Windows 11 og VoiceOver på macOS 15.4 — med Chrome 138, Firefox 130 og Safari 17.6. Hver celle registrerer den adfærd, vi observerede på tværs af ca. 20 forsøg pr. kombination. »OK« betyder, at annonceringen fyrede pålideligt med den forventede tekst. »Delvis« betyder, at den fyrede i nogle konfigurationer men ikke alle. »Fejler« betyder, at mindst én skærmlæser stille droppede annonceringen.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast-notifikation (polite)DelvisOKOKOK
Toast-notifikation (assertive)DelvisOKDelvisOK
Formulafejl (polite)OKOKOKOK
Asynkron indlæsningstilstandDelvisDelvisFejlerOK
Live data — langsom strømOKOKOKOK
Live data — udbrud (mere end 5/sek.)FejlerDelvisFejlerDelvis

Tre observationer fra matricen. For det første håndterer alle frameworks formulafejl-mønsteret korrekt ud af boksen — det er det ene kanoniske mønster, der ikke stresser reconcileren, fordi regionen monteres ved app-start og teksten ændres én gang pr. indsendelse. For det andet kæmper alle frameworks med bursty live-data, fordi ingen client-side scheduler er hurtig nok til at fodre mutationer til tilgængelighed-træet med den hastighed, det underliggende signal fyrer. For det tredje gør Svelte 5’s compile-time deduplikering indlæsningstilstand-mønsteret til en direkte fejl snarere end delvis — det eneste af de fire, hvor standardadfærden er forkert.

Skærmlæser-kolonnen har også betydning. JAWS 2026 er den strengeste af de tre vedrørende tomme regioner: den vil nægte at annoncere en region, hvis tekst ændrer sig fra "" til “Gemt”, hvis ændringen lander i samme paint som regionens mount, i alle frameworks. NVDA 2025.1 annoncerer inkonsekvent for samme tilfælde. VoiceOver på macOS 15.4 er den mest tilgivende — den annoncerer normalt selv et same-paint mount-plus-tekst — men dens tilgivelse har skjult mange framework-fejl for udviklere, der kun tester på en Mac.

Den tværgående løsning

På tværs af alle fire frameworks er den enkelte intervention, der vender flest “Delvis”-celler til “OK”, at montere én global, tom div aria-live=“polite” og én div aria-live=“assertive” i roden af appen — og route alle annonceringer gennem dem ved at skrive tekst ind i deres barn. Dette omgår alle reconciler-mount race conditions i ét træk.


5. God-vs-dårlig kode i hvert framework

Følgende par viser den forkerte og den rigtige måde at skrive indlæsningstilstand-mønsteret i hvert framework. Vi valgte indlæsningstilstand, fordi det er det mønster, where matricen viser mest rødt — og kløften mellem dårlig og god er størst.

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

Regionen monteres kun under indlæsning. Reacts automatiske batching kan committe mount og unmount inde i samme paint som dataarrivalen — og JAWS, NVDA og VoiceOver er uenige om, hvad de skal gøre med det. Nettoeffekt: »Indlæser…« siges nogle gange, andre gange ikke, uden et klientsynligt mønster.

React 19 · gør
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} />}
    </>
  );
}

Regionen monteres ved første rendering og forbliver monteret. Reacts renderer kan batche alt hvad den vil — det eneste der ændrer sig er teksten inde i en eksisterende region, hvilket er præcis hvad specifikationen beskriver.

Vue 3.5 · gør ikke
<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 to skrivninger til status kan lande i samme Vue scheduler-tick, hvis netværkssvaret er hurtigt (cached) — Vue deduplikerer, og kun den endelige streng når DOM. »Indlæser…«-annonceringen mistes stille.

Vue 3.5 · gør
<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() tvinger scheduleren til at skylle »Indlæser…« ind i DOM, før den anden tildeling sættes i kø. Skærmlæseren ser to adskilte mutationer, annoncerer dem begge.

Svelte 5 · gør ikke
<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’s compiler udsender en DOM-tekstskrivning pr. $state-ændring, men deduplikerer på hinanden følgende identiske strenge. Hvis en anden kald til load() skriver »Indlæser…« igen, udsender compileren ingen mutation — skærmlæseren hører ingenting ved andet klik.

Svelte 5 · gør
<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>

Sekvenstelleren garanterer, at hver skrivning er en ny streng. Brugeren hører ikke nummeret — skærmlæseren udjævner det — men compileren tvinges til at udsende en ny DOM-mutation hver gang. Deduplikeringsomgåelsen er hele pointen.

SolidJS 2.0 · gør ikke
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-signalet opdateres inde i batch() side om side med resultat-signalet. Solid udsætter begge DOM-skrivninger, til batchen lukker — og på et hurtigt cached svar kan »Indlæser…« og »Indlæst…« skylle i samme mikrotask. Den mellemliggende annoncering mistes.

SolidJS 2.0 · gør
async function load() {
  setStatus('Loading...');
  // status-signalet fyrer straks, uden for nogen batch
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

»Indlæser…«-skrivningen sker uden for batch(), så Solids fine-grained scheduler opdaterer DOM, i det øjeblik signalet fyrer. Skærmlæseren ser annonceringen, inden netværksturen er overstået. »Indlæst«-skrivningen kan forblive inde i batch — annonceringen fyrer stadig, fordi batchen lukker synkront around den.


6. Den tværgående spillebog

1

Mount én global live-region pr. politeness-niveau ved app-boot

Render to tomme divs — én med aria-live=“polite”, én med aria-live=“assertive” — i roden af din applikation, inden nogen rute renderes. Alle annonceringer i appen skriver ind i én af disse to regioner. Dette eliminerer mount-race-betingelsen i alle frameworks ovenfor.

2

Skriv en lille announcer-service, der indkapsler de globale regioner

Eksponer én funktion — announce(message, politeness) — der finder den tilsvarende globale region og sætter dens textContent. Frameworks kan give dig en reaktiv ref til regionen, men announceren kan blot kalde el.textContent = ” først og derefter el.textContent = message på den næste opgave, hvilket tvinger en mutation selv for identiske strenge.

3

Begræns bursty datakilder til ca. 1 besked pr. 1500 ms

Hvis din datakilde kan fyre mere end én gang pr. sekund — en scoreticker, et chat-feed — kan skærmlæserens synthesizer ikke følge med uanset framework. Saml opdateringer på klientsiden og udsend én sammenfattende besked (»3 nye beskeder«) frem for tre sekventielle annonceringer. Matricen ovenfor viser, at alle frameworks fejler »udbrud«-rækken, så løsningen skal ligge over frameworket, ikke inde i det.

4

Test med NVDA, JAWS og VoiceOver — alle tre, hver gang

Matricen ville ikke eksistere, hvis én skærmlæser var tilstrækkelig. JAWS’ strenghed over for tomme regioner og VoiceOvers tilgivelse trækker i modsatte retninger; NVDA befinder sig imellem. Et mønster, der kun annoncerer korrekt under VoiceOver — standarden for Mac-shop frontend-teams — er ødelagt for flertallet af skærmlæserbrugere.

5

Stop med at montere live-regionen betinget

Den hyppigste fejl på tværs af alle fire frameworks. Mount regionen tom ved app-start. Skift teksten. Unmount aldrig.


Konklusion: aria-live er et framework-problem forklædt som et markup-problem

At læse W3C ARIA-specifikationen giver det indtryk, at aria-live er et markup-valg — polite eller assertive, med role status eller log eller alert, og du er færdig. Specifikationen er korrekt, i den forstand at det er de eneste håndtag, specifikationen anerkender. Specifikationen er også vildledende, fordi den antager en DOM, der muterer, som et imperativt dokument muterer.

Alle frameworks ovenfor introducerer en scheduler mellem din kode og DOM, og alle schedulere har grænsetilfælde, som specifikationen ikke adresserer — automatisk batching, mikrotask-skylninger, compile-time deduplikering, signal-grafer. Grænsetilfældene er ikke fejl i frameworks; de er by-design-funktioner, der tilfældigvis interagerer dårligt med de antagelser, skærmlæsere gør om, hvornår DOM-mutationer opstår.

Løsningen er strukturel, ikke per-komponent. Mount globale live-regioner ved app-start, route alle annonceringer gennem en lille service, begræns bursty kilder, test på alle tre skærmlæsere. Det faktum, at den samme femtrinsspilebog fungerer på tværs af React, Vue, Svelte og Solid, er det stærkeste bevis for, at det framework, du valgte, betyder mindre end den arkitektur, du bygger rundt om det.

For det bredere udviklertoolkit — testmønstre, build-time-tjek, resten af frontend tilgængeligheds-kortet — se udviklernes landingsside; den komplette WCAG 2.2-succeskriterierreference indekserer de kriterier, hvert mønster ovenfor berører; den gratis WCAG 2.2-scanner opdager de strukturelle fejl, axe kan se på enhver URL, du peger den på.

»aria-live-specifikationen antager, at DOM muterer, som specifikationen skrev i 2008. Fire frameworks senere muterer ingen af dem på den måde — og skærmlæseren ved det ikke.«

— Disability World engineering desk, maj 2026