Animación de avatar de IA accesible y rápida en la web
Por el equipo de Charmotion · Actualizado el

Un avatar de IA que se reproduce en loop junto a una conversación necesita una forma de pausarse o detenerse según WCAG 2.2.2, una pose fija cuando prefers-reduced-motion está activado y un estado en texto para lectores de pantalla. Para el rendimiento, usa una sola instancia de Rive, páusala cuando esté fuera de pantalla o la pestaña esté oculta, guarda el .riv en caché y mantén el canvas montado.
Puntos clave
- El movimiento que empieza solo, dura más de cinco segundos y aparece junto a otro contenido necesita un mecanismo para pausarlo, detenerlo u ocultarlo.
- Con movimiento reducido, muestra una imagen fija del estado actual y mantén el significado en texto.
- Pausa la instancia de Rive con IntersectionObserver y la Page Visibility API; Rive recomienda pausar los gráficos fuera de pantalla.
- Oculta el canvas a las tecnologías de asistencia y anuncia solo los cambios significativos en una región de estado no intrusiva (polite).
¿Ya tienes un personaje o logo? Mira tu personaje en pantallas reales de una app: pantallas de splash, onboarding, carga y estado vacío. Es gratis, funciona en tu navegador y no sube nada.
¿Un avatar en loop permanente incumple WCAG 2.2.2?
Puede incumplirlo. El criterio de conformidad 2.2.2 de WCAG 2.2 (Pausar, detener, ocultar, nivel A) se aplica al contenido en movimiento que empieza automáticamente, dura más de cinco segundos y se presenta en paralelo con otro contenido. Para ese contenido, debe existir una forma de que el usuario lo pause, lo detenga o lo oculte (W3C). Un loop en reposo (idle) que se reproduce junto a un chat durante toda la sesión cumple las tres condiciones.
Formas de cumplirlo, de la más simple a la más flexible:
- Vincula el movimiento a la actividad. Anima mientras el asistente escucha, piensa o habla, y pasa a una pose fija en pocos segundos cuando la conversación queda en silencio. El movimiento que se detiene en menos de cinco segundos queda fuera del criterio; un loop en reposo que sigue reproduciéndose, no.
- Ofrece un control. Un interruptor "Pausar animación" cerca del avatar, o una opción para mostrar un ícono estático, cumple el criterio directamente y cubre las respuestas largas.
- Ocúltalo. Permitir que los usuarios contraigan el panel del asistente o desactiven el avatar también es un mecanismo válido.
Los archivos de Charmotion incluyen un estado de ánimo sleeping (8) que funciona bien como estado de reposo con poco movimiento antes de pausar: el personaje se va calmando de forma visible, y pausarlo en ese punto se percibe como intencional. Charmotion lo genera, junto con los otros nueve estados de ánimo y seis reacciones, a partir de tu propio personaje o logo.
¿Cómo debe responder el avatar a prefers-reduced-motion?
prefers-reduced-motion: reduce te indica que el usuario pidió a su dispositivo minimizar el movimiento no esencial (MDN). Los loops ambientales de un avatar no son esenciales por definición. El significado que transmiten, como "thinking" o "listening", sí lo es, así que pásalo a texto.
"use client";
import { useEffect, useState } from "react";
export function usePrefersReducedMotion(): boolean {
const [reduce, setReduce] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduce(mq.matches);
const onChange = () => setReduce(mq.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
return reduce;
}
export function Avatar({ mood, label }: { mood: number; label: string }) {
const reduce = usePrefersReducedMotion();
return (
<div className="flex items-center gap-2">
{reduce
? <img src="/avatar-still.png" alt="" width={64} height={64} />
: <LiveAvatar mood={mood} />}
<span role="status">{label}</span>
</div>
);
}
Renderizar una imagen fija es la opción más predecible, y además evita la descarga del runtime. No te limites a pausar la animación en vivo en el fotograma en que esté; un loop pausado a mitad del movimiento puede congelarse en una pose extraña. Usa una imagen fija creada a partir de un fotograma limpio de reposo (idle). Si quieres que el estado siga visible, alterna entre unas pocas imágenes fijas (reposo, escuchando, hablando) en lugar de animar.
El hook detecta los cambios, así que los usuarios que cambian la opción a mitad de sesión obtienen el comportamiento correcto sin recargar.
¿Cómo pauso el avatar cuando está fuera de pantalla o la pestaña está oculta?
Las buenas prácticas de Rive indican que, si un gráfico queda fuera de pantalla al hacer scroll y no necesita seguir reproduciéndose, debes llamar a su API de pausa (buenas prácticas de Rive). Combina un IntersectionObserver para la visibilidad en la página con la Page Visibility API para las pestañas en segundo plano (MDN, MDN):
"use client";
import { useEffect, useRef } from "react";
import { useRive } from "@rive-app/react-webgl2";
export function LiveAvatar() {
const box = useRef<HTMLDivElement>(null);
const { rive, RiveComponent } = useRive({
src: "/character.riv",
stateMachine: "CharacterViewModelMachine",
autoplay: true,
autoBind: true,
});
useEffect(() => {
const el = box.current;
if (!rive || !el) return;
let onScreen = true;
const sync = () => {
if (onScreen && document.visibilityState === "visible") rive.play();
else rive.pause();
};
const io = new IntersectionObserver(([entry]) => {
onScreen = entry.isIntersecting;
sync();
});
io.observe(el);
document.addEventListener("visibilitychange", sync);
return () => {
io.disconnect();
document.removeEventListener("visibilitychange", sync);
};
}, [rive]);
return (
<div ref={box} aria-hidden="true" style={{ width: 96, height: 96 }}>
<RiveComponent />
</div>
);
}
Usa la misma función sync para la pausa por inactividad: agrega un indicador de "asleep" e inclúyelo en la condición. Cuando llegue un mensaje nuevo, borra el indicador y llama a sync() antes de establecer el siguiente estado de ánimo.
¿Por qué compartir un único contexto WebGL entre los canvas de Rive?
Los navegadores limitan cuántos contextos WebGL puede tener una página a la vez, y crear muchos es costoso. El parámetro useOffscreenRenderer del runtime de Rive para React, que por defecto es true, hace que todas las instancias de Rive compartan un único contexto WebGL offscreen (o lo creen si no existe), y eso es lo que permite tener varias animaciones de Rive en una misma pantalla (parámetros de Rive React). Déjalo activado.
| Comprobación | Finalidad | Cómo |
|---|---|---|
| Pausar/detener/ocultar en loops largos | WCAG 2.2.2 | Pasar a sleeping y luego pausar; u ofrecer un interruptor |
| Movimiento reducido | Preferencia del usuario | Imagen fija más estado en texto |
| Pausar fuera de pantalla y en pestañas ocultas | Buena práctica de Rive; ahorra CPU y batería | IntersectionObserver + visibilitychange |
| Contexto WebGL compartido | Límites de contextos | Mantén useOffscreenRenderer en su valor predeterminado |
| Una sola instancia en vivo | Cada instancia renderiza cada fotograma | Avatar en vivo para el turno actual, imágenes fijas para el historial |
| Guardar el .riv en caché | Analizar y decodificar una sola vez | Reutiliza el archivo cargado en instancias repetidas |
| Canvas estable | Los remontajes reinician la animación | Mantén useRive y RiveComponent juntos |
El contexto compartido ayuda con los límites, pero cada instancia sigue costando tiempo de renderizado en cada fotograma que reproduce. Un chat que muestra un avatar en vivo junto a cada mensaje multiplica ese costo por la longitud de la conversación. Un solo avatar en vivo, más una imagen estática junto a los mensajes anteriores, es más económico y se ve más tranquilo. Si necesitas el mismo archivo en varios lugares, Rive recomienda guardar el .riv en caché para que se analice y decodifique una sola vez (buenas prácticas de Rive).
¿Cómo evito que React reinicie la animación?
La guía de Rive para React es directa: si React desmonta o recrea el canvas repetidamente, la animación puede reiniciarse, desaparecer o no mostrarse (buenas prácticas de Rive React). Las interfaces de chat son especialmente propensas a esto, porque se vuelven a renderizar con cada token transmitido.
- Mantén
useRiveyRiveComponenten un solo componente. No crees la instancia en un componente padre para renderizar el canvas en un hijo. - Monta el avatar una sola vez en un lugar estable, como el header del chat, en lugar de dentro de una lista que se vuelve a renderizar.
- Evita cambiar las keys. Una
keyligada al id del mensaje actual remonta el canvas en cada turno. - Contrólalo con valores, no con componentes nuevos. Cambia el estado de ánimo a través del view model; no reemplaces un componente de avatar por otro en cada estado.
- No lo muestres y ocultes con renderizado condicional para estados breves. Ocultarlo con CSS o pausarlo conserva el archivo cargado y la posición.
La guía del avatar de chat con streaming muestra un componente completo construido de esta forma.
¿Qué deberían escuchar los lectores de pantalla?
El canvas en sí no transmite nada a un lector de pantalla, y no debería intentarlo. Marca el contenedor del avatar con aria-hidden="true", como en los ejemplos anteriores, para que las tecnologías de asistencia lo omitan.
En su lugar, pon la información en texto:
- Una etiqueta de estado visible, como "Escuchando", "Pensando" o "Hablando", junto al avatar o al campo de entrada. También ayuda a los usuarios videntes que no están mirando el avatar.
- Una región activa en modo polite (
role="status") para esa etiqueta, de modo que los cambios se anuncien sin interrumpir. - Anuncia solo los cambios relevantes. El estado de pensar puede durar una fracción de segundo en respuestas rápidas. Anunciar cada transición genera ruido; suele bastar con anunciar "Pensando" solo tras un breve retraso, y la respuesta en sí cuando llegue.
- No describas la animación. "El robot está saludando" no aporta nada. "Conectado", sí.
Las reacciones de error requieren especial cuidado: la reacción es decoración, pero el error es información. Acompáñala siempre con un mensaje de texto y evita los efectos de destellos. Consulta animación accesible y movimiento reducido para conocer las reglas WCAG más generales.

Cómo hacer un avatar de IA accesible y eficiente
- Oculta el canvas a las tecnologías de asistencia. Envuelve el canvas de Rive en un contenedor con
aria-hidden="true"y muestra un estado en texto en un elementorole="status". - Respeta el movimiento reducido. Comprueba
prefers-reduced-motiony muestra una imagen fija de la pose en reposo (idle) en lugar del avatar en vivo cuando esté configurado enreduce. - Pausa cuando no sea visible. Llama a
rive.pause()cuando un IntersectionObserver indique que el avatar está fuera de pantalla o la página quede oculta, y arive.play()cuando vuelva a verse. - Pasa a reposo tras la inactividad. Tras un período sin conversación, cambia a un estado de sueño o de reposo (idle) y luego pausa el renderizado hasta el siguiente mensaje.
- Mantén una sola instancia estable. Monta un único avatar en vivo fuera de la lista de mensajes, mantén juntos
useRiveyRiveComponent, y deja activadouseOffscreenRenderer.
Preguntas frecuentes
¿Un avatar de chatbot animado necesita un botón de pausa?
Si se reproduce en loop durante más de cinco segundos junto a otro contenido y empieza automáticamente, la WCAG 2.2.2 exige una forma de pausarlo, detenerlo u ocultarlo. Un interruptor sirve. Pasar a una pose fija pocos segundos después de que la conversación se detenga también mantiene breve el movimiento en reposo, pero los loops largos de habla siguen beneficiándose de un control.
¿Debo pausar un avatar de Rive cuando sale de la pantalla con el scroll?
Sí. Las buenas prácticas de Rive recomiendan llamar a pause cuando un gráfico está fuera de pantalla y no necesita seguir reproduciéndose. Usa un IntersectionObserver para detectar la visibilidad y la Page Visibility API para las pestañas en segundo plano, y llama a rive.pause() y rive.play() según cambien.
¿Qué debería mostrar un avatar con movimiento reducido?
Una imagen fija de una pose limpia, normalmente idle, con el estado actual mostrado como texto, por ejemplo Escuchando o Pensando. Evita pausar la animación en vivo en un fotograma cualquiera. Si el estado debe seguir visible, alterna unas pocas imágenes estáticas en lugar de animar entre ellas.
¿Puedo mostrar varios avatares de Rive en una misma página?
Sí. El runtime de React comparte por defecto un único contexto WebGL fuera de pantalla entre instancias mediante useOffscreenRenderer, lo que evita los límites de contextos del navegador. Aun así, cada instancia en reproducción consume tiempo de renderizado, así que en un chat anima solo el avatar actual y usa imágenes estáticas para los mensajes anteriores.
Respuestas rápidas de esta guía
Fuentes
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- prefers-reduced-motion, MDN Web Docs
- Best practices, Rive
- React runtime: Parameters and return values, Rive
- React runtime: Best practices, Rive
- Intersection Observer API, MDN Web Docs
- Page Visibility API, MDN Web Docs