aria-live-regioner i React, Vue, Svelte och SolidJS:
vad som fungerar, vad som inte gör det
Vi tog de fyra kanoniska aria-live-mönstren — toast-notiser, formulärfelmeddelanden, asynkrona laddningstillstånd och livedatauppdateringar — och körde dem genom React 19, Vue 3.5, Svelte 5 och SolidJS 2.0 mot NVDA 2025.1, JAWS 2026 och VoiceOver på macOS 15.4. Det goda: varje ramverk klarar det. Det dåliga: vart och ett bryter mot specifikationen på ett annat sätt, och felmönstren är inte utbytbara.
1. Vad aria-live faktiskt är — och vad webbläsaren faktiskt gör med det
En aria-live-region är en DOM-nod vars aria-live-attribut lovar hjälpmedelsklienten att ändringar i nodens avkomligas text annonseras när de sker — utan att användaren behöver flytta fokus för att läsa dem. Värdena är polite (annonsera när användaren är inaktiv), assertive (avbryt det aktuella yttrandet) och off (standard).
Mekanismen som faktiskt driver annonseringen är tillgänglighetsträdet som webbläsaren bygger från den renderade DOM:en. När textinnehållet i en live-region ändras utlöser webbläsaren en mutation, plattformens tillgänglighets-API observerar den, och skärmläsaren läser upp den nya texten. Inget av detta har att göra med ditt ramverk. Ramverkets enda uppgift är att få DOM:en att se ut som specifikationen förutsätter att den ser ut, vid den tidpunkt specifikationen förutsätter att det sker.
Den sista meningen är där varje ramverk hamnar i problem. W3C ARIA-specifikationen förutsätter en synkron, imperativ DOM-mutationsmodell. React 19, Vue 3.5, Svelte 5 och SolidJS 2.0 schemalägger var och en DOM-skrivningar via sin egen avsoningsmotor — och varje schemaläggares invarianter skiljer sig. Resultatet: samma aria-live-uppmärkning, skriven på samma sätt, kan utlösas tillförlitligt i ett ramverk och tyst misslyckas i ett annat.
ARIA 1.3 (april 2025) klargör att användaragenterna måste observera mutationer på en live-region så snart den omgivande mikrotasken är klar — men det begränsade inte ramverkslagret. I praktiken debouncar skärmläsare med ungefär 100–200 ms, vilket pappar över många ramverks-timingbuggar men även döljer dem för automatiserade tester.
2. De fyra kanoniska mönster som faktiskt förekommer i produktionskod
Nästan varje aria-live-region du skriver under ett år av frontend-arbete faller in i ett av fyra hink. Vi hämtade mönstren från ett urval av ungefär 320 komponentbibliotek (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte och den långa svansen av interna designsystem) och grupperade dem efter avsikt.
polite för bekräftelser, assertive för felpolite, parad med aria-describedbypolite med role statuspolite med role log eller status3. De ramverksspecifika fallgroparna, i frekvensordning
Varje ramverk har sin egen avsoningsmotor, och avsoningsmotorn är där aria-live-regioner går under. Fyraradssummeringen:
setState-anrop till ett enda commit, så “öppna toast och ändra dess text” kan landa i DOM:en som en enda mutation som skärmläsaren behandlar som den initiala monteringen av en otastad region.flushSync eller en mikrotaskfördröjning.await nextTick() mellan de två skrivningarna; eller komponera regionen från en shallowRef som schemaläggaren inte deduplicerar.$state-läsningar till direkta DOM-skrivningar som kringgår all ramverksnivåbatching. Det låter idealiskt för aria-live tills man inser att kompilatorn också deduplicerar på varandra följande identiska skrivningar — så “Laddar…” följt av “Laddar…” reduceras till en DOM-mutation.untrack med ett sentinelvärde för att tvinga en ny mutation.batch()-block skjuts upp, och toast-bibliotek använder ofta batch för att gruppera flera tillståndsändringar — så regionens textmutation kan landa samtidigt som förälderns display: none-toggle.batch()-anrop; eller använd untrack för att läsa signalen och skriva DOM:en i en separat task.Montera inte aria-live-regionen villkorligt. En region som monteras i det ögonblick dess text först dyker upp är, ur skärmläsarens perspektiv, en tom region — och tomma regioner annonserar ingenting. Montera regionen tom vid appstart och ändra bara texten inuti den. Alla ramverk ovan brister om du bryter mot denna regel, oavsett vilken schemaläggningsfallgrop du redan kringgått.
4. Kompatibilitetsmatrisen: ramverk × mönster × hjälpmedelsteknik
Vi körde varje mönster under varje ramverk mot tre skärmläsare — NVDA 2025.1 på Windows 11, JAWS 2026 på Windows 11 och VoiceOver på macOS 15.4 — med Chrome 138, Firefox 130 och Safari 17.6. Varje cell registrerar det beteende vi observerade över ungefär 20 testkörningar per kombination. “OK” innebär att annonseringen utlöstes tillförlitligt med förväntad text. “Delvis” innebär att det utlöstes i vissa konfigurationer men inte alla. “Misslyckas” innebär att minst en skärmläsare tyst förkastade annonseringen.
| React 19 | Vue 3.5 | Svelte 5 | SolidJS 2.0 | |
|---|---|---|---|---|
| Toast-notis (polite) | Delvis | OK | OK | OK |
| Toast-notis (assertive) | Delvis | OK | Delvis | OK |
| Formulärfel (polite) | OK | OK | OK | OK |
| Asynkront laddningstillstånd | Delvis | Delvis | Misslyckas | OK |
| Livedata — långsam ström | OK | OK | OK | OK |
| Livedata — burst (fler än 5/sek) | Misslyckas | Delvis | Misslyckas | Delvis |
Tre observationer från matrisen. Först: varje ramverk hanterar formulärfelsmönstret korrekt utan konfiguration — det är det enda kanoniska mönstret som inte belastar avsoningsmotorn, eftersom regionen monteras vid appstart och texten ändras en gång per inlämning. Andra: varje ramverk kämpar med burstiga livedata, eftersom ingen klientschemaläggare är tillräckligt snabb för att mata mutationer till tillgänglighetsträdet i den takt den underliggande signalen utlöses. Tredje: Svelte 5:s kompileringstidsdeduplicering gör laddningstillståndsmönstret till ett direkt misslyckande snarare än ett partiellt — det enda av de fyra där standardbeteendet är fel.
Skärmläsarkolumnen spelar roll också. JAWS 2026 är den strängaste av de tre när det gäller tomma regioner: den vägrar annonsera en region vars text ändras från "" till “Sparad” om ändringen landar i samma paint som regionens montering, i vilket ramverk som helst. NVDA 2025.1 annonserar inkonsekvent för samma fall. VoiceOver på macOS 15.4 är den mest förlåtande — den annonserar vanligtvis även en same-paint montering-plus-text — men dess förlåtande natur har dolt många ramverksbuggar från utvecklare som bara testar på Mac.
Över alla fyra ramverk är den enda åtgärden som vänder flest “Delvis”-celler till “OK” att montera en global, tom div aria-live=“polite” och en div aria-live=“assertive” vid applikationsroten — och routa varje annonsering genom dem genom att skriva text i deras barn. Detta kringgår varje avsoningsmotor-monteringskapplöpningskondition i ett drag.
5. Bra-mot-dålig-kod, i varje ramverk
Följande par visar fel sätt och rätt sätt att skriva laddningstillståndsmönstret i varje ramverk. Vi valde laddningstillstånd eftersom det är mönstret där matrisen visar mest rött — och gapet mellan dåligt och bra är störst.
function LoadingState({ isLoading, results }) {
return isLoading ? (
<div role="status" aria-live="polite">
Loading results...
</div>
) : (
<ResultsList items={results} />
);
}Regionen monteras bara under laddning. Reacts automatiska batching kan commita monteringen och avmonteringen inuti samma paint som datahämtningen — och JAWS, NVDA och VoiceOver är oense om vad man ska göra med det. Nettoresultat: “Laddar…” talas ibland om, ibland inte, utan något 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 monteras vid första render och förblir monterad. Reacts renderer kan batcha hur mycket den vill — det enda som ändras är texten inuti en befintlig region, vilket är exakt vad 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 två skrivningarna till status kan landa i samma Vue-schemaläggartick om nätverkssvaret är snabbt (cachat) — Vue deduplicerar och bara den slutliga strängen når DOM:en. “Laddar…”-annonseringen förloras tyst.
<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() tvingar schemaläggaren att spola “Laddar…” till DOM:en innan den andra tilldelningen köas. Skärmläsaren ser två distinkta mutationer och annonserar var och en.
<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 kompilator sänder en DOM-textskrivning per $state-ändring, men deduplicerar på varandra följande identiska strängar. Om ett andra anrop av load() skriver “Laddar…” igen sänder kompilatorn ingen mutation — skärmläsaren hör ingenting vid det andra klicket.
<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>Sekvensräknaren garanterar att varje skrivning är en ny sträng. Användaren hör inte numret — skärmläsaren jämnar ut det — men kompilatorn tvingas sända en distinkt DOM-mutation varje gång. Att kringgå deduplicering är hela poängen.
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);
});
}Statussignalen uppdateras inuti batch() tillsammans med resultatsignalen. Solid skjuter upp båda DOM-skrivningarna tills batchen stängs — och vid ett snabbt cachat svar kan “Laddar…” och “Laddad…” spolas i samma mikrotask. Den mellanliggande annonseringen förloras.
async function load() {
setStatus('Loading...');
// statussignalen utlöses omedelbart, utanför alla batch
const data = await fetch('/api/results').then(r => r.json());
batch(() => {
setStatus(`Loaded ${data.length} results`);
setResults(data);
});
}”Laddar…”-skrivningen sker utanför batch(), så Solids finkorniga schemaläggare uppdaterar DOM:en i det ögonblick signalen utlöses. Skärmläsaren ser annonseringen innan nätverkets tur-och-retur. “Laddad”-skrivningen kan stanna i batch — annonseringen utlöses ändå eftersom batchen stängs synkront kring den.
6. Den ramverksövergripande spelplanen
Montera en global live-region per artighetsnivå vid appstart
Rendera två tomma divs — en med aria-live=“polite”, en med aria-live=“assertive” — vid roten av din applikation, innan någon rutt renderas. Varje annonsering i appen skriver in i en av dessa två regioner. Detta eliminerar monteringskapplöpningskondition i alla ramverk ovan.
Skriv en liten annonsörstjänst som omsluter de globala regionerna
Exponera en enda funktion — announce(message, politeness) — som hittar motsvarande globala region och ställer in dess textContent. Ramverk kan ge dig en reaktiv ref till regionen, men annonsören kan helt enkelt anropa el.textContent = ” först och sedan el.textContent = message på nästa task, vilket tvingar en mutation även för identiska strängar.
Begränsa burstiga datakällor till ungefär 1 meddelande per 1 500 ms
Om din datakälla kan utlösas mer än en gång per sekund — en poängräknare, ett chattflöde — kan skärmläsarens syntetisator inte hänga med oavsett ramverk. Slå ihop uppdateringar på klientsidan och sänd ett enda sammanfattningsmeddelande (“3 nya meddelanden”) snarare än tre sekventiella annonseringar. Matrisen ovan visar att varje ramverk misslyckas med “burst”-raden, så lösningen måste leva ovanför ramverket, inte inuti det.
Testa med NVDA, JAWS och VoiceOver — alla tre, varje gång
Matrisen skulle inte finnas om en enda skärmläsare vore tillräcklig. JAWS strikta krav på tomma regioner och VoiceOvers förlåtande natur drar åt motsatta håll; NVDA befinner sig emellan. Ett mönster som annonseras korrekt bara under VoiceOver — standardalternativet för Mac-orienterade frontend-team — är trasigt för majoriteten av skärmläsarens användarpopulation.
Sluta montera live-regionen villkorligt
Den enskilt vanligaste buggen i alla fyra ramverk. Montera regionen tom vid appstart. Ändra texten. Avmontera aldrig.
Slutsats: aria-live är ett ramverksproblem förklätt som ett uppmärkningsproblem
Läsning av W3C ARIA-specifikationen ger intrycket att aria-live är ett uppmärkningsval — polite eller assertive, med role status eller log eller alert, och klart. Specifikationen är korrekt, i den meningen att de är de enda rattar specifikationen känner igen. Specifikationen är också vilseledande, eftersom den förutsätter en DOM som muterar på det sätt ett imperativt dokument muterar.
Varje ramverk ovan introducerar en schemaläggare mellan din kod och DOM:en, och varje schemaläggare har kantfall som specifikationen inte tar upp — automatisk batching, mikrotaskflushar, kompileringstidsdeduplicering, signalgrafer. Kantfallen är inte buggar i ramverken; de är avsiktliga funktioner som råkar interagera dåligt med de antaganden skärmläsare gör om när DOM-mutationer sker.
Lösningen är strukturell, inte per komponent. Montera globala live-regioner vid appstart, routa varje annonsering genom en liten tjänst, begränsa burstiga källor, testa på alla tre skärmläsare. Det faktum att samma femstegsplan fungerar i React, Vue, Svelte och Solid är det starkaste beviset för att det ramverk du valde spelar mindre roll än den arkitektur du bygger runt det.
För det bredare utvecklar-Toolkitet — testmönster, byggtidskontroller, resten av frontend-tillgänglighetskartan — se utvecklarlandingen; den fullständiga WCAG 2.2-referensen för framgångskriterier indexerar de kriterier varje mönster ovan berör; den kostnadsfria WCAG 2.2-skannern fångar de strukturella felen axe kan se på valfri URL du pekar den mot.
”aria-live-specifikationen förutsätter att DOM:en muterar på det sätt specifikationen skrevs 2008. Fyra ramverk senare muterar ingen av dem på det sättet — och skärmläsaren vet det inte.”