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.

Teknisk introduktion · aria-live i olika ramverk

aria-live-regioner i React, Vue, Svelte och SolidJS: vad som fungerar, vad som inte gör det

Vi testade aria-live-regioner i React 19, Vue 3.5, Svelte 5 och SolidJS 2.0 — fyra kanoniska mönster, tre skärmläsare, alla ramverksspecifika fallgropar. Beteendematrisen, bra-mot-dålig-kod och spelplanen.

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.

4
testade ramverk
12
ramverk-mönster-kombinationer
3
verifierade skärmläsare
14 min läsning
Uppdaterad maj 2026

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.

Specifikation kontra implementation

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.

Mönster 1 · Toast-notis
”Sparad”, “Kopierad till urklipp”, feltoasts
Ungefär 78 % av de granskade komponentbiblioteken levererar en toast
Live-inställningpolite för bekräftelser, assertive för fel
RiskMontering/avmontering-thrash dödar annonseringen
Mönster 2 · Formulärfelmeddelande
Inline-validering under ett fält
Krävs av WCAG 3.3.1 i interaktiva formulär
Live-inställningpolite, parad med aria-describedby
RiskRegion monteras bara vid fel — “ingen region, ingen annonsering”
Mönster 3 · Asynkront laddningstillstånd
”Laddar resultat…”, snurrar, skelett
Ungefär hälften av de granskade biblioteken gör det fel
Live-inställningpolite med role status
RiskText byts för fort — bara slutläget läses upp
Mönster 4 · Livedatauppdateringar
Poängräknare, chattmeddelanden, köräknare
Det svåraste av de fyra att få rätt
Live-inställningpolite med role log eller status
RiskUppdateringsburstet överväldigar syntetisatorn — “kösläpp”

3. De ramverksspecifika fallgroparna, i frekvensordning

Varje ramverk har sin egen avsoningsmotor, och avsoningsmotorn är där aria-live-regioner går under. Fyraradssummeringen:

React 19
Concurrent renderer · automatisk batching
Den vanligaste källan till “toasten talades inte om”-felrapporter
FallgropAutomatisk batching slår ihop två 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.
LösningMontera regionen tom vid första render, skriv sedan text vid nästa paint med flushSync eller en mikrotaskfördröjning.
Vue 3.5
Reaktivitetsstyrd schemaläggare · nextTick
Subtilare — fel ser ut som “regionen annonserades men med fel text”
FallgropVues schemaläggare spolar DOM-uppdateringar på nästa mikrotask efter en tillståndsändring. En laddningstext som skrivs och sedan omedelbart ersätts inuti samma tick når DOM:en bara i sin slutform — den mellanliggande “laddar”-strängen observeras aldrig.
LösningAnvänd await nextTick() mellan de två skrivningarna; eller komponera regionen från en shallowRef som schemaläggaren inte deduplicerar.
Svelte 5
Runes · kompileringstidsreaktivitet
Annorlunda form av bugg — kompilatorn är både problemet och lösningen
FallgropSvelte 5 kompilerar $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.
LösningLägg till en osynlig räknare i live-regiontexten vid varje uppdatering; eller använd untrack med ett sentinelvärde för att tvinga en ny mutation.
SolidJS 2.0
Finkorniga signaler · ingen VDOM
Närmast de fyra av “fungerar bara” — men har sin egen kantfall
FallgropSolids signalgraf uppdaterar DOM-noder synkront när en signal utlöses, vilket är bra för aria-live. Men signaler som utlöses inuti ett 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.
LösningHåll live-regionens ägarsignal utanför alla batch()-anrop; eller använd untrack för att läsa signalen och skriva DOM:en i en separat task.
Vanlig fallgrop · alla fyra ramverk

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 19Vue 3.5Svelte 5SolidJS 2.0
Toast-notis (polite)DelvisOKOKOK
Toast-notis (assertive)DelvisOKDelvisOK
Formulärfel (polite)OKOKOKOK
Asynkront laddningstillståndDelvisDelvisMisslyckasOK
Livedata — långsam strömOKOKOKOK
Livedata — burst (fler än 5/sek)MisslyckasDelvisMisslyckasDelvis

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.

Den övergripande lösningen

Ö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.

React 19 · gör inte
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.

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 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.

Vue 3.5 · gör inte
<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.

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() 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.

Svelte 5 · gör inte
<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.

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>

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.

SolidJS 2.0 · gör inte
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.

SolidJS 2.0 · gör
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

1

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.

2

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.

3

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.

4

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.

5

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.”

— Disability World ingenjörsredaktion, maj 2026