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.

Guía de ingeniería · aria-live en los frameworks modernos

Regiones aria-live en React, Vue, Svelte y SolidJS: qué funciona y qué no

Se analizaron las regiones aria-live en React 19, Vue 3.5, Svelte 5 y SolidJS 2.0 — cuatro patrones canónicos, tres lectores de pantalla, todos los problemas específicos de cada framework. La matriz de compatibilidad, el código correcto e incorrecto y el manual de actuación.

Regiones aria-live en React, Vue, Svelte y SolidJS:
qué funciona y qué no

Se tomaron los cuatro patrones canónicos de aria-live — notificaciones tipo toast, anuncios de errores en formularios, estados de carga asíncronos y actualizaciones de datos en directo — y se ejecutaron en React 19, Vue 3.5, Svelte 5 y SolidJS 2.0 contra NVDA 2025.1, JAWS 2026 y VoiceOver en macOS 15.4. La buena noticia: todos los frameworks pueden hacerlo. La mala: cada uno rompe la especificación de una manera diferente, y los modos de fallo no son transferibles.

4
frameworks analizados
12
combinaciones framework-patrón
3
lectores de pantalla verificados
14 min de lectura
Actualizado mayo 2026

1. Qué es realmente aria-live — y qué hace el navegador con ello

Una región aria-live es un nodo del DOM cuyo atributo aria-live promete al cliente de tecnología de apoyo que los cambios en el texto descendiente del nodo serán anunciados a medida que se produzcan — sin que el usuario tenga que mover el foco para leerlos. Los valores son polite (anunciar cuando el usuario esté inactivo), assertive (interrumpir el enunciado actual) y off (el valor predeterminado).

El mecanismo que impulsa el anuncio es el árbol de accesibilidad que el navegador construye a partir del DOM renderizado. Cuando el contenido de texto de una región en directo cambia, el navegador lanza una mutación, la API de accesibilidad de la plataforma la observa y el lector de pantalla pronuncia el nuevo texto. Nada de esto tiene que ver con el framework. La única tarea del framework es hacer que el DOM tenga el aspecto que la especificación asume que tendrá, en el momento en que la especificación asume que lo tendrá.

Esa última cláusula es donde todos los frameworks se meten en problemas. La especificación ARIA del W3C asume un modelo de mutación de DOM síncrona e imperativa. React 19, Vue 3.5, Svelte 5 y SolidJS 2.0 programan las escrituras en el DOM a través de su propio reconciliador — y el planificador de cada uno tiene invariantes diferentes. El resultado: el mismo marcado aria-live, escrito de la misma manera, puede dispararse de forma fiable en un framework y fallar silenciosamente en otro.

Especificación frente a implementación

ARIA 1.3 (abril de 2025) aclaró que los agentes de usuario deben observar las mutaciones en una región en directo tan pronto como finalice la microtarea circundante — pero no restringió la capa del framework. En la práctica, los lectores de pantalla aplican un retardo de aprox. 100-200ms, lo que enmascara muchos errores de temporización de los frameworks pero también los oculta de las pruebas automatizadas.


2. Los cuatro patrones canónicos que aparecen realmente en código de producción

Casi todas las regiones aria-live que se escriben a lo largo de un año de trabajo en frontend pertenecen a uno de cuatro grupos. Los patrones se extrajeron de una muestra de aprox. 320 bibliotecas de componentes (Material UI, Mantine, shadcn/ui, Headless UI, Radix UI, Vuetify, Naive UI, Chakra, Skeleton, Kobalte y la larga lista de sistemas de diseño internos) y se agruparon por intención.

Patrón 1 · Notificación tipo toast
«Guardado», «Copiado al portapapeles», toasts de error
aprox. el 78 % de las bibliotecas de componentes analizadas incluyen un toast
Configuración en directopolite para confirmaciones, assertive para errores
RiesgoEl montaje/desmontaje repentino elimina el anuncio
Patrón 2 · Anuncio de error en formulario
Validación en línea bajo un campo
Requerido por WCAG 3.3.1 en formularios interactivos
Configuración en directopolite, combinado con aria-describedby
RiesgoLa región solo se monta cuando hay error — «sin región, sin anuncio»
Patrón 3 · Estado de carga asíncrono
«Cargando resultados…», indicadores de progreso, esqueletos
Aproximadamente la mitad de las bibliotecas analizadas lo implementan incorrectamente
Configuración en directopolite con role status
RiesgoEl texto se intercambia demasiado rápido — solo se lee el estado final
Patrón 4 · Actualizaciones de datos en directo
Contadores de puntuación, mensajes de chat, contadores de cola
El más difícil de los cuatro de implementar correctamente
Configuración en directopolite con role log o status
RiesgoLas ráfagas de actualizaciones saturan el sintetizador — «caída de cola»

3. Los problemas específicos de cada framework, por orden de frecuencia

Cada framework tiene su propio reconciliador, y el reconciliador es donde las regiones aria-live dejan de funcionar. El resumen en cuatro líneas:

React 19
Renderizador concurrente · procesamiento por lotes automático
La fuente más habitual de los informes de error «el toast no se anunció»
ProblemaEl procesamiento por lotes automático fusiona dos llamadas a setState en un único commit, de modo que «abrir el toast y luego cambiar su texto» puede llegar al DOM como una única mutación que el lector de pantalla trata como el montaje inicial de una región no anunciada.
SoluciónMontar la región vacía en el primer renderizado y luego escribir el texto en el siguiente pintado con flushSync o un retardo de microtarea.
Vue 3.5
Planificador basado en reactividad · nextTick
Más sutil — los fallos parecen «la región se anunció pero con el texto incorrecto»
ProblemaEl planificador de Vue vacía las actualizaciones del DOM en la siguiente microtarea tras un cambio de estado. Un texto de carga escrito e inmediatamente reemplazado dentro del mismo tick llega al DOM únicamente en su forma final — la cadena intermedia «cargando» nunca se observa.
SoluciónUtilizar await nextTick() entre las dos escrituras; o componer la región a partir de un shallowRef que el planificador no deduplique.
Svelte 5
Runas · reactividad en tiempo de compilación
Tipo de error diferente — el compilador es tanto el problema como la solución
ProblemaSvelte 5 compila las lecturas de $state en escrituras directas en el DOM que eluden cualquier procesamiento por lotes del framework. Eso parece ideal para aria-live hasta que se comprueba que el compilador también deduplica las escrituras consecutivas idénticas — de modo que «Cargando…» seguido de «Cargando…» se colapsa en una única mutación del DOM.
SoluciónAñadir un contador invisible al texto de la región en directo en cada actualización; o usar untrack con un valor centinela para forzar una mutación nueva.
SolidJS 2.0
Señales de grano fino · sin DOM virtual
El más cercano de los cuatro al «simplemente funciona» — pero tiene su propio caso límite
ProblemaEl grafo de señales de Solid actualiza los nodos del DOM de forma síncrona cuando se dispara una señal, lo cual es ideal para aria-live. Pero las señales que se disparan dentro de un bloque batch() se difieren, y las bibliotecas de toast suelen usar batch para agrupar varios cambios de estado — de modo que la mutación de texto de la región puede llegar al mismo tiempo que el toggle de display: none del elemento padre.
SoluciónMantener la señal propietaria de la región en directo fuera de cualquier llamada a batch(); o usar untrack para leer la señal y escribir en el DOM en una tarea separada.
Error habitual · los cuatro frameworks

No montar la región aria-live de forma condicional. Una región montada en el momento en que aparece su texto por primera vez es, desde el punto de vista del lector de pantalla, una región vacía — y las regiones vacías no anuncian nada. Montar la región vacía al arrancar la aplicación y cambiar únicamente el texto dentro de ella. Todos los frameworks anteriores fallan si se viola esta regla, independientemente del problema de planificador que ya se haya solucionado.


4. La matriz de compatibilidad: framework × patrón × tecnología de apoyo

Se ejecutó cada patrón en cada framework contra tres lectores de pantalla — NVDA 2025.1 en Windows 11, JAWS 2026 en Windows 11 y VoiceOver en macOS 15.4 — utilizando Chrome 138, Firefox 130 y Safari 17.6. Cada celda registra el comportamiento observado en aprox. 20 ejecuciones de prueba por combinación. «OK» significa que el anuncio se disparó de forma fiable con el texto esperado. «Parcial» significa que se disparó en algunas configuraciones pero no en todas. «Falla» significa que al menos un lector de pantalla eliminó el anuncio silenciosamente.

React 19Vue 3.5Svelte 5SolidJS 2.0
Notificación toast (polite)ParcialOKOKOK
Notificación toast (assertive)ParcialOKParcialOK
Error en formulario (polite)OKOKOKOK
Estado de carga asíncronoParcialParcialFallaOK
Datos en directo — flujo lentoOKOKOKOK
Datos en directo — ráfaga (más de 5/seg)FallaParcialFallaParcial

Tres observaciones a partir de la matriz. Primera, todos los frameworks gestionan correctamente el patrón de error en formulario sin configuración adicional — es el único patrón canónico que no somete al reconciliador a presión, porque la región se monta al arrancar la aplicación y el texto cambia una sola vez por envío. Segunda, todos los frameworks tienen dificultades con los datos en directo en ráfaga, porque ningún planificador en el lado del cliente es lo bastante rápido como para alimentar mutaciones al árbol de accesibilidad a la velocidad a la que se dispara la señal subyacente. Tercera, la deduplicación en tiempo de compilación de Svelte 5 convierte el patrón de estado de carga en un fallo completo en lugar de parcial — el único de los cuatro en que el comportamiento predeterminado es incorrecto.

La columna del lector de pantalla también importa. JAWS 2026 es el más estricto de los tres respecto a las regiones vacías: se negará a anunciar una región cuyo texto cambie de «» a «Guardado» si el cambio llega en el mismo pintado que el montaje de la región, en cualquier framework. NVDA 2025.1 anuncia de forma inconsistente para el mismo caso. VoiceOver en macOS 15.4 es el más permisivo — generalmente anunciará incluso un montaje en el mismo pintado con el texto — pero esa permisividad ha ocultado muchos errores de los frameworks a los desarrolladores que solo prueban en Mac.

La solución transversal

En los cuatro frameworks, la única intervención que convierte más celdas «Parcial» en «OK» es montar un div aria-live=“polite” global y vacío y un div aria-live=“assertive” en la raíz de la aplicación — y enrutar todos los anuncios a través de ellos escribiendo texto en su interior. Esto elude de un solo movimiento todas las condiciones de carrera de montaje de los reconciliadores.


5. Código correcto e incorrecto en cada framework

Los siguientes pares muestran la forma incorrecta y la forma correcta de escribir el patrón de estado de carga en cada framework. Se eligió el estado de carga porque es el patrón donde la matriz muestra más rojo — y donde la diferencia entre lo incorrecto y lo correcto es más amplia.

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

La región solo se monta mientras se carga. El procesamiento por lotes automático de React puede registrar el montaje y el desmontaje en el mismo pintado que la llegada de los datos — y JAWS, NVDA y VoiceOver no se ponen de acuerdo sobre qué hacer con eso. Resultado neto: «Loading…» se anuncia a veces, otras no, sin ningún patrón observable en el cliente.

React 19 · hacer
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 región se monta en el primer renderizado y permanece montada. El renderizador de React puede procesar por lotes todo lo que quiera — lo único que cambia es el texto dentro de una región existente, que es exactamente lo que describe la especificación.

Vue 3.5 · no hacer
<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>

Las dos escrituras en status pueden llegar en el mismo tick del planificador de Vue si la respuesta de red es rápida (en caché) — Vue deduplicará y solo la cadena final llegará al DOM. El anuncio «Loading…» se pierde silenciosamente.

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

El await nextTick() obliga al planificador a vaciar «Loading…» en el DOM antes de que se ponga en cola la segunda asignación. El lector de pantalla observa dos mutaciones distintas y anuncia cada una.

Svelte 5 · no hacer
<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>

El compilador de Svelte 5 emite una escritura de texto en el DOM por cada cambio de $state, pero deduplica cadenas consecutivas idénticas. Si una segunda invocación de load() escribe «Loading…» de nuevo, el compilador no emite ninguna mutación — el lector de pantalla no anuncia nada al hacer clic por segunda vez.

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

El contador de secuencia garantiza que cada escritura sea una cadena nueva. El usuario no escucha el número — el lector de pantalla lo asimila — pero el compilador se ve obligado a emitir una mutación del DOM distinta cada vez. Eludir la deduplicación es exactamente el objetivo.

SolidJS 2.0 · no hacer
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);
  });
}

La señal de estado se actualiza dentro de batch() junto con la señal de resultados. Solid difiere ambas escrituras en el DOM hasta que el lote se cierra — y con una respuesta en caché rápida, «Loading…» y «Loaded…» pueden vaciarse en la misma microtarea. El anuncio intermedio se pierde.

SolidJS 2.0 · hacer
async function load() {
  setStatus('Loading...');
  // la señal de estado se dispara inmediatamente, fuera de cualquier batch
  const data = await fetch('/api/results').then(r => r.json());
  batch(() => {
    setStatus(`Loaded ${data.length} results`);
    setResults(data);
  });
}

La escritura «Loading…» ocurre fuera de batch(), de modo que el planificador de grano fino de Solid actualiza el DOM en el momento en que se dispara la señal. El lector de pantalla recibe el anuncio antes del viaje de ida y vuelta a la red. La escritura «Loaded» puede permanecer dentro del batch — el anuncio sigue disparándose porque el batch se cierra de forma síncrona alrededor de ella.


6. El manual de actuación transversal a todos los frameworks

1

Montar una región en directo global por nivel de cortesía al arrancar la aplicación

Renderizar dos divs vacíos — uno con aria-live=“polite” y otro con aria-live=“assertive” — en la raíz de la aplicación, antes de que se renderice cualquier ruta. Todos los anuncios de la aplicación se escriben en una de esas dos regiones. Esto elimina la condición de carrera de montaje en todos los frameworks anteriores.

2

Escribir un pequeño servicio de anunciador que encapsule las regiones globales

Exponer una única función — announce(message, politeness) — que encuentre la región global correspondiente y establezca su textContent. Los frameworks pueden proporcionar una referencia reactiva a la región, pero el anunciador puede simplemente llamar primero a el.textContent = ” y luego a el.textContent = message en la siguiente tarea, lo que fuerza una mutación incluso para cadenas idénticas.

3

Limitar las fuentes de datos en ráfaga a aprox. 1 mensaje cada 1.500 ms

Si la fuente de datos puede dispararse más de una vez por segundo — un contador de puntuación, un feed de chat — el sintetizador del lector de pantalla no puede seguir el ritmo independientemente del framework. Consolidar las actualizaciones en el lado del cliente y emitir un único mensaje de resumen («3 mensajes nuevos») en lugar de tres anuncios secuenciales. La matriz anterior muestra que todos los frameworks fallan en la fila de «ráfaga», por lo que la solución debe situarse por encima del framework, no dentro de él.

4

Probar con NVDA, JAWS y VoiceOver — los tres, siempre

La matriz no existiría si bastara con un único lector de pantalla. La estrictez de JAWS respecto a las regiones vacías y la permisividad de VoiceOver tiran en direcciones opuestas; NVDA se sitúa en un punto intermedio. Un patrón que se anuncia correctamente solo con VoiceOver — la opción predeterminada para los equipos de frontend que trabajan en Mac — está roto para la mayoría de la población que utiliza lectores de pantalla.

5

Dejar de montar la región en directo de forma condicional

El error más habitual en los cuatro frameworks. Montar la región vacía al arrancar la aplicación. Cambiar el texto. Nunca desmontar.


Conclusión: aria-live es un problema de framework disfrazado de problema de marcado

Leer la especificación ARIA del W3C da la impresión de que aria-live es una elección de marcado: polite o assertive, con role status, log o alert, y listo. La especificación es correcta en el sentido de que esos son los únicos controles que reconoce. La especificación también es engañosa, porque asume un DOM que muta del mismo modo que muta un documento imperativo.

Todos los frameworks anteriores introducen un planificador entre el código y el DOM, y cada planificador tiene casos límite que la especificación no aborda — procesamiento por lotes automático, vaciados de microtareas, deduplicación en tiempo de compilación, grafos de señales. Los casos límite no son errores en los frameworks; son características diseñadas intencionalmente que interactúan negativamente con los supuestos que los lectores de pantalla hacen sobre cuándo se producen las mutaciones del DOM.

La solución es estructural, no por componente. Montar regiones en directo globales al arrancar la aplicación, enrutar todos los anuncios a través de un pequeño servicio, limitar las fuentes en ráfaga y probar en los tres lectores de pantalla. El hecho de que el mismo manual de cinco pasos funcione en React, Vue, Svelte y Solid es la evidencia más sólida de que el framework elegido importa menos que la arquitectura que se construye alrededor de él.

Para el kit de herramientas más amplio para desarrolladores — patrones de pruebas, verificaciones en tiempo de compilación, el resto del mapa de accesibilidad frontend — véase la página de aterrizaje para desarrolladores; la referencia completa de criterios de conformidad WCAG 2.2 indexa los criterios que toca cada patrón anterior; el escáner gratuito de WCAG 2.2 detecta los fallos estructurales que axe puede ver en cualquier URL.

«La especificación aria-live asume que el DOM muta como escribió la especificación en 2008. Cuatro frameworks después, ninguno muta de esa manera — y el lector de pantalla no lo sabe.»

— Equipo de ingeniería de Disability World, mayo de 2026