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 dans les frameworks

Les régions aria-live dans React, Vue, Svelte et SolidJS : ce qui fonctionne, ce qui ne fonctionne pas

Nous avons testé les régions aria-live dans React 19, Vue 3.5, Svelte 5 et SolidJS 2.0 — quatre patterns canoniques, trois lecteurs d'écran, tous les pièges propres à chaque framework. Voici la matrice de compatibilité, le bon et le mauvais code, et le guide pratique.

Les régions aria-live dans React, Vue, Svelte et SolidJS :
ce qui fonctionne, ce qui ne fonctionne pas

Nous avons pris les quatre patterns aria-live canoniques — notifications toast, annonces d’erreurs de formulaire, états de chargement asynchrone et mises à jour de données en direct — et les avons exécutés dans React 19, Vue 3.5, Svelte 5 et SolidJS 2.0, face à NVDA 2025.1, JAWS 2026 et VoiceOver sur macOS 15.4. La bonne nouvelle : chaque framework peut le faire. La mauvaise : chacun enfreint la spécification d’une manière différente, et les modes d’échec ne sont pas transférables.

4
frameworks testés
12
combinaisons framework-pattern
3
lecteurs d’écran vérifiés
14 min de lecture
Mis à jour mai 2026

1. Ce qu’est réellement aria-live — et ce que le navigateur en fait concrètement

Une région aria-live est un nœud du DOM dont l’attribut aria-live promet au client de technologie d’assistance que les modifications apportées au texte descendant du nœud seront annoncées dès qu’elles se produisent — sans que l’utilisateur ait à déplacer le focus pour les lire. Les valeurs sont polite (annonce lorsque l’utilisateur est inactif), assertive (interrompt l’énonciation en cours) et off (la valeur par défaut).

Le mécanisme qui pilote réellement l’annonce est l’arbre d’accessibilité que le navigateur construit à partir du DOM rendu. Lorsque le contenu textuel d’une région live change, le navigateur déclenche une mutation, l’API d’accessibilité de la plateforme l’observe et le lecteur d’écran lit le nouveau texte. Tout cela n’a rien à voir avec le framework utilisé. Le seul rôle du framework est de faire en sorte que le DOM ressemble à ce que la spécification attend, au moment où elle l’attend.

C’est cette dernière clause qui pose problème à tous les frameworks. La spécification ARIA du W3C suppose un modèle de mutation DOM synchrone et impératif. React 19, Vue 3.5, Svelte 5 et SolidJS 2.0 planifient chacun leurs écritures DOM via leur propre réconciliateur — et chaque planificateur a ses propres invariants. Résultat : le même balisage aria-live, écrit de la même façon, peut se déclencher de manière fiable dans un framework et échouer silencieusement dans un autre.

Spécification et implémentation

ARIA 1.3 (avril 2025) a précisé que les agents utilisateurs doivent observer les mutations sur une région live dès la fin de la microtâche environnante — mais sans contraindre la couche framework. En pratique, les lecteurs d’écran appliquent un délai antirebond d’environ 100 à 200 ms, ce qui masque de nombreux bugs de timing des frameworks, mais les dissimule également aux tests automatisés.


2. Les quatre patterns canoniques que l’on rencontre réellement en production

Presque toutes les régions aria-live que vous écrirez au cours d’une année de développement frontend entrent dans l’une de ces quatre catégories. Nous avons tiré les patterns d’un échantillon d’environ 320 bibliothèques de composants (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte, et la longue traîne des design systems maison) et les avons regroupés par intention.

Pattern 1 · Notification toast
”Enregistré”, “Copié dans le presse-papiers”, toasts d’erreur
environ 78 % des bibliothèques de composants testées proposent un toast
Paramètre livepolite pour les confirmations, assertive pour les erreurs
RisqueLe montage/démontage répété tue l’annonce
Pattern 2 · Annonce d’erreur de formulaire
Validation en ligne sous un champ
Requis par WCAG 3.3.1 dans les formulaires interactifs
Paramètre livepolite, associé à aria-describedby
RisqueLa région n’est montée qu’en cas d’erreur — « pas de région, pas d’annonce »
Pattern 3 · État de chargement asynchrone
”Chargement des résultats…”, spinners, squelettes
Environ la moitié des bibliothèques testées l’implémentent incorrectement
Paramètre livepolite avec le role status
RisqueTexte remplacé trop vite — seul l’état final est lu
Pattern 4 · Mises à jour de données en direct
Compteurs de score, messages de chat, files d’attente
Le plus difficile des quatre à réaliser correctement
Paramètre livepolite avec le role log ou status
RisqueLes rafales de mises à jour saturent le synthétiseur — « perte de file »

3. Les pièges propres à chaque framework, par ordre de fréquence

Chaque framework possède son propre réconciliateur, et c’est là que les régions aria-live rencontrent leurs problèmes. Le résumé en quatre lignes :

React 19
Renderer concurrent · regroupement automatique
La source la plus fréquente du bug « le toast n’a pas été lu »
PiègeLe regroupement automatique fusionne deux appels setState en un seul commit, de sorte que « ouvrir le toast puis modifier son texte » peut atterrir dans le DOM comme une seule mutation que le lecteur d’écran traite comme le montage initial d’une région muette.
CorrectionMonter la région vide au premier rendu, puis écrire le texte au prochain paint avec flushSync ou un délai de microtâche.
Vue 3.5
Planificateur piloté par la réactivité · nextTick
Plus subtil — les échecs ressemblent à « la région a annoncé, mais avec le mauvais texte »
PiègeLe planificateur de Vue vide les mises à jour DOM à la prochaine microtâche après un changement d’état. Un texte de chargement écrit puis immédiatement remplacé dans le même tick n’atteint le DOM que sous sa forme finale — la chaîne « chargement » intermédiaire n’est jamais observée.
CorrectionUtiliser await nextTick() entre les deux écritures ; ou composer la région à partir d’un shallowRef que le planificateur ne déduplique pas.
Svelte 5
Runes · réactivité à la compilation
Bug de forme différente — le compilateur est à la fois le problème et la solution
PiègeSvelte 5 compile les lectures de $state en écritures DOM directes qui contournent tout regroupement au niveau du framework. C’est idéal pour aria-live en apparence, jusqu’à ce que l’on réalise que le compilateur déduplique aussi les écritures identiques consécutives — « Chargement… » suivi de « Chargement… » est réduit à une seule mutation DOM.
CorrectionAjouter un compteur invisible au texte de la région live à chaque mise à jour ; ou utiliser untrack avec une valeur sentinelle pour forcer une nouvelle mutation.
SolidJS 2.0
Signaux à grain fin · sans VDOM
Le plus proche du « fonctionne sans configuration » des quatre — mais avec son propre cas particulier
PiègeLe graphe de signaux de Solid met à jour les nœuds DOM de manière synchrone lorsqu’un signal se déclenche, ce qui est excellent pour aria-live. Mais les signaux déclenchés à l’intérieur d’un bloc batch() sont différés, et les bibliothèques de toast utilisent souvent batch pour regrouper plusieurs changements d’état — la mutation de texte de la région peut donc atterrir en même temps que le basculement display: none du parent.
CorrectionMaintenir le signal propriétaire de la région live hors de tout appel batch() ; ou utiliser untrack pour lire le signal et écrire le DOM dans une tâche séparée.
Piège commun · les quatre frameworks

Ne montez pas la région aria-live de manière conditionnelle. Une région montée au moment où son texte apparaît pour la première fois est, du point de vue du lecteur d’écran, une région vide — et les régions vides n’annoncent rien. Montez la région vide au démarrage de l’application et ne changez jamais que le texte à l’intérieur. Tous les frameworks ci-dessus échouent si vous enfreignez cette règle, quel que soit le piège de planificateur que vous avez déjà contourné.


4. La matrice de compatibilité : framework × pattern × technologie d’assistance

Nous avons exécuté chaque pattern sous chaque framework face à trois lecteurs d’écran — NVDA 2025.1 sur Windows 11, JAWS 2026 sur Windows 11 et VoiceOver sur macOS 15.4 — en utilisant Chrome 138, Firefox 130 et Safari 17.6. Chaque cellule enregistre le comportement observé sur environ 20 passages par combinaison. « OK » signifie que l’annonce s’est déclenchée de manière fiable avec le texte attendu. « Partiel » signifie qu’elle s’est déclenchée dans certaines configurations mais pas toutes. « Échoue » signifie qu’au moins un lecteur d’écran a silencieusement manqué l’annonce.

React 19Vue 3.5Svelte 5SolidJS 2.0
Toast (polite)PartielOKOKOK
Toast (assertive)PartielOKPartielOK
Erreur de formulaire (polite)OKOKOKOK
État de chargement asynchronePartielPartielÉchoueOK
Données en direct — flux lentOKOKOKOK
Données en direct — rafale (plus de 5/s)ÉchouePartielÉchouePartiel

Trois observations ressortent de la matrice. Premièrement, chaque framework gère correctement le pattern d’erreur de formulaire sans configuration particulière — c’est le seul pattern canonique qui ne stress pas le réconciliateur, car la région est montée au démarrage de l’application et le texte ne change qu’une fois par soumission. Deuxièmement, chaque framework peine face aux données en rafale, car aucun planificateur côté client n’est assez rapide pour alimenter des mutations dans l’arbre d’accessibilité au rythme auquel le signal sous-jacent se déclenche. Troisièmement, la déduplication à la compilation de Svelte 5 transforme le pattern d’état de chargement en un échec pur plutôt qu’en un résultat partiel — le seul des quatre où le comportement par défaut est incorrect.

La colonne du lecteur d’écran est également significative. JAWS 2026 est le plus strict des trois concernant les régions vides : il refusera d’annoncer une région dont le texte passe de « » à « Enregistré » si le changement atterrit dans le même paint que le montage de la région, quel que soit le framework. NVDA 2025.1 annonce de manière incohérente dans le même cas. VoiceOver sur macOS 15.4 est le plus indulgent — il annoncera généralement même un montage-plus-texte dans le même paint — mais cette indulgence a longtemps caché des bugs de framework aux développeurs qui ne testent que sur Mac.

La correction transversale

Pour l’ensemble des quatre frameworks, l’intervention unique qui fait basculer le plus de cellules « Partiel » vers « OK » consiste à monter un div aria-live=“polite” global et vide, ainsi qu’un div aria-live=“assertive”, à la racine de l’application — et à acheminer chaque annonce en écrivant le texte dans l’un d’eux. Cette approche contourne en un seul geste toutes les conditions de course liées au montage dans les réconciliateurs.


5. Bon et mauvais code, pour chaque framework

Les paires suivantes montrent la mauvaise façon et la bonne façon d’écrire le pattern d’état de chargement dans chaque framework. Nous avons choisi l’état de chargement car c’est le pattern où la matrice présente le plus de rouge — et l’écart entre mauvais et bon code est le plus large.

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

La région n’est montée que pendant le chargement. Le regroupement automatique de React peut valider le montage et le démontage dans le même paint que l’arrivée des données — et JAWS, NVDA et VoiceOver ne s’accordent pas sur la conduite à tenir. Résultat : « Chargement… » est parfois lu, parfois non, sans pattern visible côté client.

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

La région est montée au premier rendu et reste montée. Le renderer de React peut regrouper à volonté — la seule chose qui change est le texte à l’intérieur d’une région existante, ce qui correspond exactement à ce que la spécification décrit.

Vue 3.5 · à éviter
<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>

Les deux écritures sur status peuvent atterrir dans le même tick du planificateur Vue si la réponse réseau est rapide (mise en cache) — Vue les déduplique et seule la chaîne finale atteint le DOM. L’annonce « Chargement… » est silencieusement perdue.

Vue 3.5 · à faire
<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>

Le await nextTick() force le planificateur à vider « Chargement… » dans le DOM avant que la seconde affectation ne soit mise en file. Le lecteur d’écran voit deux mutations distinctes et annonce chacune.

Svelte 5 · à éviter
<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>

Le compilateur de Svelte 5 émet une écriture DOM par changement de $state, mais déduplique les chaînes identiques consécutives. Si une deuxième invocation de load() écrit « Chargement… » à nouveau, le compilateur n’émet aucune mutation — le lecteur d’écran n’entend rien au second clic.

Svelte 5 · à faire
<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>

Le compteur de séquence garantit que chaque écriture est une chaîne nouvelle. L’utilisateur n’entend pas le numéro — le lecteur d’écran l’absorbe — mais le compilateur est contraint d’émettre une mutation DOM distincte à chaque fois. Contourner la déduplication est tout le propos.

SolidJS 2.0 · à éviter
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);
  });
}

Le signal de statut est mis à jour à l’intérieur de batch() aux côtés du signal de résultats. Solid diffère les deux écritures DOM jusqu’à la fermeture du batch — sur une réponse mise en cache rapide, « Chargement… » et « Chargé… » peuvent se vider dans la même microtâche. L’annonce intermédiaire est perdue.

SolidJS 2.0 · à faire
async function load() {
  setStatus('Loading...');
  // le signal de statut se déclenche immédiatement, hors de tout batch
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

L’écriture « Chargement… » se produit hors du batch(), de sorte que le planificateur à grain fin de Solid met à jour le DOM dès que le signal se déclenche. Le lecteur d’écran voit l’annonce avant l’aller-retour réseau. L’écriture « Chargé » peut rester dans un batch — l’annonce se déclenche quand même car le batch se ferme de manière synchrone.


6. Le guide pratique multi-framework

1

Monter une région live globale par niveau de politesse, au démarrage de l’application

Rendre deux divs vides — l’un avec aria-live=“polite”, l’autre avec aria-live=“assertive” — à la racine de l’application, avant que toute route soit rendue. Chaque annonce de l’application écrit dans l’une de ces deux régions. Cela élimine la condition de course au montage dans tous les frameworks ci-dessus.

2

Écrire un petit service d’annonce qui encapsule les régions globales

Exposer une fonction unique — announce(message, politeness) — qui trouve la région globale correspondante et définit son textContent. Les frameworks peuvent fournir une ref réactive vers la région, mais le service peut simplement appeler el.textContent = ” puis el.textContent = message à la tâche suivante, ce qui force une mutation même pour des chaînes identiques.

3

Limiter les sources de données en rafale à environ 1 message toutes les 1 500 ms

Si une source de données peut se déclencher plus d’une fois par seconde — un compteur de score, un fil de chat — le synthétiseur du lecteur d’écran ne peut pas suivre, quel que soit le framework. Il convient de regrouper les mises à jour côté client et d’émettre un seul message de synthèse (« 3 nouveaux messages ») plutôt que trois annonces consécutives. La matrice ci-dessus montre que tous les frameworks échouent sur la ligne « rafale », donc la correction doit se situer au-dessus du framework, pas à l’intérieur.

4

Tester avec NVDA, JAWS et VoiceOver — les trois, à chaque fois

La matrice n’existerait pas si un seul lecteur d’écran suffisait. La rigueur de JAWS concernant les régions vides et la tolérance de VoiceOver tirent dans des directions opposées ; NVDA se situe entre les deux. Un pattern qui s’annonce correctement uniquement sous VoiceOver — la valeur par défaut pour les équipes frontend de boutiques Mac — est défaillant pour la majorité de la population utilisant des lecteurs d’écran.

5

Cesser de monter la région live de manière conditionnelle

Le bug le plus répandu dans les quatre frameworks. Monter la région vide au démarrage de l’application. Changer le texte. Ne jamais démonter.


Conclusion : aria-live est un problème de framework déguisé en problème de balisage

La lecture de la spécification ARIA du W3C laisse l’impression qu’aria-live est un choix de balisage — polite ou assertive, avec le role status, log ou alert, et le tour est joué. La spécification est correcte, dans le sens où ce sont les seuls réglages qu’elle reconnaît. Elle est aussi trompeuse, car elle suppose un DOM qui mute comme un document impératif.

Chaque framework ci-dessus introduit un planificateur entre votre code et le DOM, et chaque planificateur a des cas particuliers que la spécification n’aborde pas — regroupement automatique, vidange de microtâches, déduplication à la compilation, graphes de signaux. Ces cas particuliers ne sont pas des bugs dans les frameworks ; ce sont des fonctionnalités conçues délibérément qui interagissent malencontreusement avec les hypothèses que font les lecteurs d’écran sur le moment où les mutations DOM se produisent.

La correction est structurelle, pas par composant. Il faut monter des régions live globales au démarrage de l’application, acheminer chaque annonce via un petit service, limiter les sources en rafale et tester sur les trois lecteurs d’écran. Le fait que le même guide pratique en cinq étapes fonctionne pour React, Vue, Svelte et Solid est la preuve la plus forte que le framework choisi importe moins que l’architecture qui l’entoure.

Pour l’ensemble des outils de développement — patterns de test, vérifications à la compilation, le reste de la carte de l’accessibilité frontend — voir la page dédiée aux développeurs ; la référence complète des critères de succès WCAG 2.2 indexe les critères que chaque pattern ci-dessus touche ; le scanner WCAG 2.2 gratuit détecte les échecs structurels qu’axe peut voir sur n’importe quelle URL.

« La spécification aria-live suppose que le DOM mute comme la spécification l’a écrit en 2008. Quatre frameworks plus tard, aucun ne mute de cette façon — et le lecteur d’écran ne le sait pas. »

— Cellule ingénierie de Disability World, mai 2026