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-Überblick · aria-live in verschiedenen Frameworks

aria-live-Regionen in React, Vue, Svelte und SolidJS: was funktioniert, was nicht

aria-live-Regionen wurden in React 19, Vue 3.5, Svelte 5 und SolidJS 2.0 getestet — vier kanonische Muster, drei Screenreader, alle framework-spezifischen Fallstricke. Hier sind die Kompatibilitätsmatrix, der gute und schlechte Code sowie das Playbook.

aria-live-Regionen in React, Vue, Svelte und SolidJS:
was funktioniert, was nicht

Die vier kanonischen aria-live-Muster — Toast-Benachrichtigungen, Formularfehler-Ankündigungen, asynchrone Ladezustände und Live-Datenaktualisierungen — wurden in React 19, Vue 3.5, Svelte 5 und SolidJS 2.0 gegen NVDA 2025.1, JAWS 2026 und VoiceOver auf macOS 15.4 getestet. Die gute Nachricht: jedes Framework kann es. Die schlechte Nachricht: Jedes bricht die Spezifikation auf eine andere Weise, und die Fehlermuster sind nicht übertragbar.

4
getestete Frameworks
12
Framework-Muster-Kombinationen
3
verifizierte Screenreader
14 Min. Lesezeit
Aktualisiert Mai 2026

1. Was aria-live eigentlich ist — und was der Browser tatsächlich damit macht

Eine aria-live-Region ist ein DOM-Knoten, dessen aria-live-Attribut dem assistiven-Technologie-Client verspricht, dass Änderungen am Nachfolger-Text des Knotens bei ihrem Auftreten angekündigt werden — ohne dass die Nutzerin oder der Nutzer den Fokus dorthin bewegen muss, um sie zu lesen. Die Werte sind polite (ankündigen, wenn der Nutzer inaktiv ist), assertive (aktuelle Äußerung unterbrechen) und off (Standard).

Der Mechanismus, der die Ankündigung tatsächlich auslöst, ist der Barrierefreiheitsbaum, den der Browser aus dem gerenderten DOM aufbaut. Wenn sich der Textinhalt einer Live-Region ändert, löst der Browser eine Mutation aus, die Plattform-Barrierefreiheits-API beobachtet sie, und der Screenreader liest den neuen Text vor. All das hat nichts mit dem Framework zu tun. Die einzige Aufgabe des Frameworks ist es, das DOM so aussehen zu lassen, wie die Spezifikation es erwartet — zu dem Zeitpunkt, zu dem die Spezifikation es erwartet.

Genau dieser letzte Teilsatz ist es, an dem jedes Framework scheitert. Die W3C-ARIA-Spezifikation geht von einem synchronen, imperativen DOM-Mutationsmodell aus. React 19, Vue 3.5, Svelte 5 und SolidJS 2.0 planen DOM-Schreibvorgänge jeweils durch ihren eigenen Reconciler — und jeder Scheduler hat unterschiedliche Invarianten. Das Ergebnis: dieselbe aria-live-Markup, auf dieselbe Weise geschrieben, kann in einem Framework zuverlässig auslösen und in einem anderen stillschweigend fehlschlagen.

Spezifikation versus Implementierung

ARIA 1.3 (April 2025) hat klargestellt, dass User-Agents Mutationen an einer Live-Region beobachten müssen, sobald der umgebende Microtask abgeschlossen ist — ohne jedoch die Framework-Schicht einzuschränken. In der Praxis entprellen Screenreader mit ca. 100–200 ms, was viele Framework-Timing-Fehler überdeckt, sie aber gleichzeitig vor automatisierten Tests verbirgt.


2. Die vier kanonischen Muster, die tatsächlich im Produktionscode vorkommen

Fast jede aria-live-Region, die in einem Jahr Frontend-Arbeit geschrieben wird, fällt in einen von vier Bereichen. Die Muster wurden aus einer Stichprobe von ca. 320 Komponentenbibliotheken entnommen (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte sowie der langen Liste interner Design-Systeme) und nach Absicht gruppiert.

Muster 1 · Toast-Benachrichtigung
”Gespeichert”, “In die Zwischenablage kopiert”, Fehler-Toasts
Ca. 78 % der untersuchten Komponentenbibliotheken enthalten einen Toast
Live-Einstellungpolite für Bestätigungen, assertive für Fehler
RisikoMount/Unmount-Thrash zerstört die Ankündigung
Muster 2 · Formularfehler-Ankündigung
Inline-Validierung unter einem Feld
Durch WCAG 3.3.1 in interaktiven Formularen erforderlich
Live-Einstellungpolite, in Kombination mit aria-describedby
RisikoRegion wird nur bei Fehler gemountet — „keine Region, keine Ankündigung“
Muster 3 · Asynchroner Ladezustand
”Ergebnisse werden geladen…”, Spinner, Skelette
Ungefähr die Hälfte der untersuchten Bibliotheken implementiert es falsch
Live-Einstellungpolite mit role status
RisikoText wird zu schnell ausgetauscht — nur der Endzustand wird vorgelesen
Muster 4 · Live-Datenaktualisierungen
Score-Ticker, Chat-Nachrichten, Warteschlangenzähler
Das schwierigste der vier Muster
Live-Einstellungpolite mit role log oder status
RisikoAktualisierungsschübe überlasten den Synthesizer — „Queue-Drop“

3. Die framework-spezifischen Fallstricke, geordnet nach Häufigkeit

Jedes Framework hat seinen eigenen Reconciler, und der Reconciler ist der Ort, an dem aria-live-Regionen scheitern. Die Kurzfassung in vier Zeilen:

React 19
Concurrent-Renderer · automatisches Batching
Die häufigste Quelle von Fehlerberichten „der Toast hat nichts gesagt“
FallstrickAutomatisches Batching fasst zwei setState-Aufrufe zu einem einzigen Commit zusammen, sodass „Toast öffnen, dann Text ändern“ als eine einzige Mutation im DOM landen kann, die der Screenreader als initiales Mount einer unangekündigten Region behandelt.
LösungDie Region beim ersten Render leer mounten, dann den Text beim nächsten Paint mit flushSync oder einer Microtask-Verzögerung schreiben.
Vue 3.5
Reaktivitätsgesteuerter Scheduler · nextTick
Subtiler — Fehler sehen aus wie „die Region hat angekündigt, aber mit dem falschen Text“
FallstrickVues Scheduler leert DOM-Updates auf dem nächsten Microtask nach einer Zustandsänderung. Ein Lade-Text, der in demselben Tick geschrieben und sofort ersetzt wird, erreicht das DOM nur in seiner endgültigen Form — der Zwischenstand „Laden“ wird nie beobachtet.
Lösungawait nextTick() zwischen den zwei Schreibvorgängen verwenden; oder die Region aus einem shallowRef zusammensetzen, den der Scheduler nicht dedupliziert.
Svelte 5
Runes · Compile-time-Reaktivität
Andere Form des Fehlers — der Compiler ist sowohl das Problem als auch die Lösung
FallstrickSvelte 5 kompiliert $state-Lesevorgänge in direkte DOM-Schreibvorgänge, die jedes Framework-Batching umgehen. Das klingt ideal für aria-live, bis deutlich wird, dass der Compiler auch aufeinanderfolgende identische Schreibvorgänge dedupliziert — sodass „Laden…“ gefolgt von „Laden…“ zu einer einzigen DOM-Mutation zusammengefasst wird.
LösungEinen unsichtbaren Zähler an den Live-Region-Text bei jeder Aktualisierung anfügen; oder untrack mit einem Sentinel-Wert verwenden, um eine neue Mutation zu erzwingen.
SolidJS 2.0
Feingranulare Signale · kein VDOM
Von den vier am nächsten an „funktioniert einfach“ — hat aber seinen eigenen Randfall
FallstrickSolids Signal-Graph aktualisiert DOM-Knoten synchron, wenn ein Signal ausgelöst wird — was für aria-live gut ist. Aber Signale, die innerhalb eines batch()-Blocks ausgelöst werden, werden verzögert, und Toast-Bibliotheken verwenden batch oft, um mehrere Zustandsänderungen zu gruppieren — sodass die Textmutation der Region gleichzeitig mit dem display: none-Toggle des übergeordneten Elements landen kann.
LösungDas besitzende Signal der Live-Region außerhalb jedes batch()-Aufrufs halten; oder untrack verwenden, um das Signal zu lesen und das DOM in einem separaten Task zu schreiben.
Häufiger Fallstrick · alle vier Frameworks

Die aria-live-Region selbst darf nicht bedingt gemountet werden. Eine Region, die in dem Moment gemountet wird, in dem ihr Text erstmals erscheint, ist aus Sicht des Screenreaders eine leere Region — und leere Regionen kündigen nichts an. Die Region beim App-Start leer mounten und danach nur den Text darin ändern. Jedes der obigen Frameworks bricht, wenn diese Regel verletzt wird — unabhängig davon, welcher Scheduler-Fallstrick bereits umgangen wurde.


4. Die Kompatibilitätsmatrix: Framework × Muster × assistive Technologie

Jedes Muster wurde unter jedem Framework gegen drei Screenreader getestet — NVDA 2025.1 unter Windows 11, JAWS 2026 unter Windows 11 und VoiceOver unter macOS 15.4 — mit Chrome 138, Firefox 130 und Safari 17.6. Jede Zelle zeigt das beobachtete Verhalten über ca. 20 Testläufe pro Kombination. „OK“ bedeutet, dass die Ankündigung zuverlässig mit dem erwarteten Text ausgelöst wurde. „Partiell“ bedeutet, dass sie in einigen Konfigurationen ausgelöst wurde, aber nicht in allen. „Fehler“ bedeutet, dass mindestens ein Screenreader die Ankündigung stillschweigend verworfen hat.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast-Benachrichtigung (polite)PartiellOKOKOK
Toast-Benachrichtigung (assertive)PartiellOKPartiellOK
Formularfehler (polite)OKOKOKOK
Asynchroner LadezustandPartiellPartiellFehlerOK
Live-Daten — langsamer StreamOKOKOKOK
Live-Daten — Burst (mehr als 5/Sek.)FehlerPartiellFehlerPartiell

Drei Beobachtungen aus der Matrix. Erstens: Jedes Framework verarbeitet das Formularfehler-Muster standardmäßig korrekt — das ist das einzige kanonische Muster, das den Reconciler nicht belastet, weil die Region beim App-Start gemountet wird und der Text sich pro Einreichung nur einmal ändert. Zweitens: Jedes Framework hat Probleme mit Burst-Live-Daten, da kein clientseitiger Scheduler schnell genug ist, um Mutationen so an den Barrierefreiheitsbaum zu liefern, wie das zugrunde liegende Signal feuert. Drittens: Sveltes 5 Compile-time-Deduplizierung macht das Ladezustand-Muster zu einem vollständigen Fehler statt nur zu einem partiellen — das einzige der vier, bei dem das Standardverhalten falsch ist.

Die Screenreader-Spalte ist ebenfalls wichtig. JAWS 2026 ist der strengste der drei bezüglich leerer Regionen: Es verweigert die Ankündigung einer Region, deren Text sich von „“ nach „Gespeichert“ ändert, wenn die Änderung in demselben Paint wie der Mount der Region landet — in jedem Framework. NVDA 2025.1 kündigt bei demselben Fall inkonsistent an. VoiceOver auf macOS 15.4 ist am nachsichtigsten — es kündigt in der Regel sogar bei einem gleichzeitigen Mount-plus-Text an — aber diese Nachsichtigkeit hat viele Framework-Fehler vor Entwicklerinnen und Entwicklern verborgen, die ausschließlich auf einem Mac testen.

Die übergreifende Lösung

Bei allen vier Frameworks ist die einzige Maßnahme, die die meisten „Partiell“-Zellen auf „OK“ kippt, das Mounten einer globalen, leeren div aria-live=“polite” und einer div aria-live=“assertive” im Root der App — und alle Ankündigungen durch das Schreiben von Text in deren Kind-Element zu leiten. Das umgeht in einem Zug jeden Reconciler-Mount-Race-Condition.


5. Guter und schlechter Code, je Framework

Die folgenden Paare zeigen die falsche und die richtige Art, das Ladezustand-Muster in jedem Framework zu schreiben. Das Ladezustand-Muster wurde gewählt, weil es das Muster ist, bei dem die Matrix am meisten Rot zeigt — und die Lücke zwischen schlecht und gut am größten ist.

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

Die Region wird nur während des Ladens gemountet. Reacts automatisches Batching kann den Mount und den Unmount innerhalb desselben Paints wie das Dateneintreffen committen — und JAWS, NVDA und VoiceOver sind sich nicht einig, was dann zu tun ist. Netto-Effekt: „Loading…“ wird manchmal gesprochen, manchmal nicht, ohne erkennbares Muster auf der Client-Seite.

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

Die Region wird beim ersten Render gemountet und bleibt gemountet. Reacts Renderer kann so viel batchen wie er will — das Einzige, was sich ändert, ist der Text innerhalb einer bestehenden Region, was genau das ist, was die Spezifikation beschreibt.

Vue 3.5 · nicht so
<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>

Die zwei Schreibvorgänge auf status können im selben Vue-Scheduler-Tick landen, wenn die Netzwerkantwort schnell ist (gecacht) — Vue dedupliziert, und nur der finale String erreicht das DOM. Die „Loading…“-Ankündigung geht stillschweigend verloren.

Vue 3.5 · so
<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>

Das await nextTick() zwingt den Scheduler, „Loading…“ in das DOM zu leeren, bevor die zweite Zuweisung in die Warteschlange gestellt wird. Der Screenreader sieht zwei unterschiedliche Mutationen und kündigt jede einzeln an.

Svelte 5 · nicht so
<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>

Sveltes 5 Compiler erzeugt einen DOM-Text-Schreibvorgang pro $state-Änderung, dedupliziert aber aufeinanderfolgende identische Strings. Wenn ein zweiter Aufruf von load() erneut „Loading…“ schreibt, erzeugt der Compiler keine Mutation — der Screenreader hört beim zweiten Klick nichts.

Svelte 5 · so
<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>

Der Sequenzzähler garantiert, dass jeder Schreibvorgang ein neuer String ist. Die Nutzerin oder der Nutzer hört die Zahl nicht — der Screenreader glättet sie heraus — aber der Compiler wird gezwungen, jedes Mal eine neue DOM-Mutation zu erzeugen. Das Umgehen der Deduplizierung ist der gesamte Zweck.

SolidJS 2.0 · nicht so
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);
  });
}

Das Status-Signal wird innerhalb von batch() zusammen mit dem Ergebnis-Signal aktualisiert. Solid verzögert beide DOM-Schreibvorgänge bis der Batch schließt — und bei einer schnellen gecachten Antwort können „Loading…“ und „Loaded…“ im selben Microtask geleert werden. Die Zwischenankündigung geht verloren.

SolidJS 2.0 · so
async function load() {
  setStatus('Loading...');
  // Status-Signal feuert sofort, außerhalb jedes batch()
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

Der „Loading…“-Schreibvorgang erfolgt außerhalb von batch(), sodass Solids feingranularer Scheduler das DOM in dem Moment aktualisiert, in dem das Signal feuert. Der Screenreader sieht die Ankündigung vor dem Netzwerk-Roundtrip. Der „Loaded“-Schreibvorgang kann im Batch bleiben — die Ankündigung feuert trotzdem, weil der Batch synchron darum schließt.


6. Das framework-übergreifende Playbook

1

Beim App-Start eine globale Live-Region pro Dringlichkeitsstufe mounten

Zwei leere Divs rendern — eines mit aria-live=“polite”, eines mit aria-live=“assertive” — im Root der Anwendung, bevor irgendeine Route gerendert wird. Jede Ankündigung in der App schreibt in eine dieser beiden Regionen. Das eliminiert die Mount-Race-Condition in jedem der obigen Frameworks.

2

Einen kleinen Announcer-Service schreiben, der die globalen Regionen kapselt

Eine einzige Funktion bereitstellen — announce(message, politeness) — die die entsprechende globale Region findet und deren textContent setzt. Frameworks können einen reaktiven Ref auf die Region bereitstellen, aber der Announcer kann einfach el.textContent = ” aufrufen und dann im nächsten Task el.textContent = message — was eine Mutation erzwingt, auch bei identischen Strings.

3

Burst-Datenquellen auf ca. 1 Nachricht pro 1.500 ms drosseln

Wenn eine Datenquelle mehr als einmal pro Sekunde feuern kann — ein Score-Ticker, ein Chat-Feed — kann der Synthesizer des Screenreaders nicht mithalten, unabhängig vom Framework. Updates clientseitig zusammenfassen und eine einzige Zusammenfassung ausgeben („3 neue Nachrichten“) statt drei aufeinanderfolgende Ankündigungen. Die Matrix oben zeigt, dass jedes Framework bei der „Burst“-Zeile scheitert — die Lösung muss also oberhalb des Frameworks liegen, nicht darin.

4

Mit NVDA, JAWS und VoiceOver testen — alle drei, jedes Mal

Die Matrix würde nicht existieren, wenn ein einziger Screenreader ausreichen würde. JAWS’ Strenge bezüglich leerer Regionen und VoiceOvers Nachsichtigkeit ziehen in entgegengesetzte Richtungen; NVDA liegt dazwischen. Ein Muster, das nur unter VoiceOver — dem Standard für Mac-Shop-Frontend-Teams — korrekt ankündigt, ist für die Mehrheit der Screenreader-nutzenden Bevölkerung defekt.

5

Die Live-Region nicht mehr bedingt mounten

Der bei weitem häufigste Fehler in allen vier Frameworks. Die Region beim App-Start leer mounten. Den Text ändern. Niemals unmounten.


Fazit: aria-live ist ein Framework-Problem, das wie ein Markup-Problem aussieht

Wer die W3C-ARIA-Spezifikation liest, bekommt den Eindruck, aria-live sei eine Markup-Entscheidung — polite oder assertive, mit role status oder log oder alert, und damit wäre es erledigt. Die Spezifikation ist korrekt, in dem Sinne, dass das die einzigen Regler sind, die sie kennt. Die Spezifikation ist aber auch irreführend, weil sie ein DOM voraussetzt, das so mutiert wie ein imperatives Dokument mutiert.

Jedes der obigen Frameworks fügt zwischen dem Code und dem DOM einen Scheduler ein, und jeder Scheduler hat Randfälle, die die Spezifikation nicht adressiert — automatisches Batching, Microtask-Flushes, Compile-time-Deduplizierung, Signal-Graphen. Die Randfälle sind keine Fehler in den Frameworks; sie sind bewusst gewählte Funktionen, die schlecht mit den Annahmen interagieren, die Screenreader über den Zeitpunkt von DOM-Mutationen machen.

Die Lösung ist strukturell, nicht komponentenweise. Globale Live-Regionen beim App-Start mounten, alle Ankündigungen über einen kleinen Service leiten, Burst-Quellen drosseln, auf allen drei Screenreadern testen. Die Tatsache, dass dasselbe fünfschrittige Playbook über React, Vue, Svelte und Solid hinweg funktioniert, ist der stärkste Hinweis darauf, dass das gewählte Framework weniger wichtig ist als die Architektur, die darum herum aufgebaut wird.

Für das breitere Entwickler-Toolkit — Testmuster, Build-time-Prüfungen und die übrige Frontend-Barrierefreiheitskarte — siehe die Entwickler-Landingpage; die vollständige WCAG-2.2-Erfolgskriterien-Referenz listet die Kriterien, die jedes der obigen Muster berührt; der kostenlose WCAG-2.2-Scanner erkennt strukturelle Fehler, die axe auf jeder angegebenen URL sehen kann.

„Die aria-live-Spezifikation setzt voraus, dass das DOM so mutiert, wie die Spezifikation es 2008 geschrieben hat. Vier Frameworks später mutiert keines von ihnen so — und der Screenreader weiß das nicht.“

— Disability World Engineering-Desk, Mai 2026