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.
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.
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.
polite til bekræftelser, assertive til fejlpolite, parret med aria-describedbypolite med role statuspolite med role log eller status3. 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:
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.flushSync eller en mikrotask-forsinkelse.await nextTick() mellem de to skrivninger; eller sammensæt regionen fra en shallowRef, som scheduleren ikke deduplikerer.$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.untrack med en sentinel-værdi for at tvinge en ny mutation.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.batch()-kald; eller brug untrack til at læse signalet og skrive DOM i en separat opgave.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 19 | Vue 3.5 | Svelte 5 | SolidJS 2.0 | |
|---|---|---|---|---|
| Toast-notifikation (polite) | Delvis | OK | OK | OK |
| Toast-notifikation (assertive) | Delvis | OK | Delvis | OK |
| Formulafejl (polite) | OK | OK | OK | OK |
| Asynkron indlæsningstilstand | Delvis | Delvis | Fejler | OK |
| Live data — langsom strøm | OK | OK | OK | OK |
| Live data — udbrud (mere end 5/sek.) | Fejler | Delvis | Fejler | Delvis |
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.
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.
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.
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.
<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.
<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.
<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.
<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.
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.
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
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.
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.
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.
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.
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.«