Animação de avatar de IA acessível e rápida na web
Por equipe Charmotion · Atualizado em

Um avatar de IA que fica em loop ao lado de uma conversa precisa de uma forma de pausar ou parar, conforme o WCAG 2.2.2, de uma pose estática quando prefers-reduced-motion estiver ativo e de um status em texto para leitores de tela. Para ter velocidade, rode uma única instância Rive, pause-a quando estiver fora da tela ou a aba estiver oculta, faça cache do .riv e mantenha o canvas montado.
Principais pontos
- Movimento que começa sozinho, dura mais de cinco segundos e aparece junto de outro conteúdo precisa de um mecanismo para pausar, parar ou ocultar.
- Com movimento reduzido, mostre uma imagem estática do estado atual e mantenha o significado em texto.
- Pause a instância Rive com IntersectionObserver e a Page Visibility API; o Rive recomenda pausar gráficos fora da tela.
- Oculte o canvas das tecnologias assistivas e anuncie apenas mudanças relevantes em uma região de status educada (polite).
Já tem um personagem ou logo? Veja seu personagem em telas reais de app: telas de splash, onboarding, carregamento e estado vazio. É grátis, roda no navegador e não envia nada.
Um avatar em loop contínuo viola o WCAG 2.2.2?
Pode violar. O Critério de Sucesso 2.2.2 do WCAG 2.2 (Pausar, parar, ocultar, Nível A) se aplica a conteúdo em movimento que começa automaticamente, dura mais de cinco segundos e é apresentado em paralelo com outro conteúdo. Para esse conteúdo, deve haver uma forma de o usuário pausá-lo, pará-lo ou ocultá-lo (W3C). Um loop idle rodando ao lado de um chat durante toda a sessão atende às três condições.
Formas de cumprir o critério, da mais simples à mais flexível:
- Vincule o movimento à atividade. Anime enquanto o assistente estiver ouvindo, pensando ou falando e volte a uma pose estática em poucos segundos quando a conversa ficar em silêncio. Movimento que para em até cinco segundos fica fora do critério; um loop idle que continua rodando, não.
- Ofereça um controle. Um botão "Pausar animação" perto do avatar, ou uma configuração para mostrar um ícone estático, atende ao critério diretamente e cobre respostas longas.
- Permita ocultar. Deixar o usuário recolher o painel do assistente ou desativar o avatar também é um mecanismo válido.
Os arquivos do Charmotion incluem um humor sleeping (8) que funciona bem como estado de repouso com pouco movimento antes da pausa: o personagem desacelera visivelmente, e pausar nele parece intencional. O Charmotion o gera, junto com os outros nove humores e seis reações, a partir do seu próprio personagem ou logo.
Como o avatar deve responder a prefers-reduced-motion?
prefers-reduced-motion: reduce indica que o usuário pediu ao dispositivo para minimizar movimentos não essenciais (MDN). Os loops de ambiente de um avatar são não essenciais por definição. Já o significado que eles transmitem, como "pensando" ou "ouvindo", é essencial, então leve esse significado para o 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 uma imagem estática é a opção mais previsível e ainda evita o download do runtime. Não pause simplesmente a animação em qualquer frame em que ela estiver; um loop pausado no meio do movimento pode congelar em uma pose estranha. Use uma imagem estática feita a partir de um frame idle limpo. Se quiser manter o estado visível, alterne entre algumas imagens estáticas (idle, listening, talking) em vez de animar.
O hook escuta mudanças, então quem alterar a configuração no meio da sessão recebe o comportamento certo sem recarregar a página.
Como pauso o avatar quando ele está fora da tela ou a aba está oculta?
As boas práticas do Rive dizem que, se um gráfico sair da tela pela rolagem e não precisar continuar tocando, você deve chamar a API de pausa dele (boas práticas do Rive). Combine um IntersectionObserver para a visibilidade na página com a Page Visibility API para abas em 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>
);
}
Use a mesma função sync para a pausa por inatividade: adicione uma flag de "adormecido" e inclua-a na condição. Quando chegar uma nova mensagem, limpe a flag e chame sync() antes de definir o próximo humor.
Por que compartilhar um único contexto WebGL entre canvases Rive?
Os navegadores limitam quantos contextos WebGL uma página pode manter ao mesmo tempo, e criar muitos é caro. O parâmetro useOffscreenRenderer do runtime React do Rive, que tem true como padrão, faz todas as instâncias Rive compartilharem um único contexto WebGL offscreen (ou criá-lo, se não existir), e é isso que permite várias animações Rive em uma mesma tela (parâmetros do Rive React). Deixe-o ativado.
| Item | Por quê | Como |
|---|---|---|
| Pausar/parar/ocultar loops longos | WCAG 2.2.2 | Passe para sleeping e depois pause; ou ofereça um botão |
| Movimento reduzido | Preferência do usuário | Imagem estática mais status em texto |
| Pausar fora da tela e em abas ocultas | Boa prática do Rive; economiza CPU e bateria | IntersectionObserver + visibilitychange |
| Contexto WebGL compartilhado | Limites de contexto | Mantenha useOffscreenRenderer no padrão |
| Uma instância ativa | Cada instância renderiza todos os frames | Avatar ativo na vez atual, imagens estáticas no histórico |
| Faça cache do .riv | Analisar e decodificar uma vez | Reutilize o arquivo carregado em novas instâncias |
| Canvas estável | Remontagens reiniciam a animação | Mantenha useRive e RiveComponent juntos |
O contexto compartilhado ajuda com os limites, mas cada instância ainda custa tempo de renderização em cada frame que toca. Um chat que mostra um avatar ativo ao lado de cada mensagem multiplica esse custo pelo tamanho da conversa. Um único avatar ativo, mais uma imagem estática ao lado das mensagens antigas, é mais leve e parece mais calmo. Se você realmente precisar do mesmo arquivo em vários lugares, o Rive recomenda fazer cache do .riv para que ele seja analisado e decodificado apenas uma vez (boas práticas do Rive).
Como evito que o React reinicie a animação?
A orientação do Rive para React é direta: se o React desmontar ou recriar o canvas repetidamente, a animação pode reiniciar, sumir ou não aparecer (boas práticas do Rive React). Interfaces de chat são especialmente propensas a isso, porque renderizam de novo a cada token transmitido.
- Mantenha
useRiveeRiveComponentem um único componente. Não crie a instância em um componente pai para renderizar o canvas em um filho. - Monte o avatar uma única vez em um lugar estável, como o header do chat, e não dentro de uma lista que é renderizada de novo.
- Evite trocar keys. Uma
keyvinculada ao id da mensagem atual remonta o canvas a cada turno da conversa. - Controle-o com valores, não com novos componentes. Mude o humor pelo view model; não troque um componente de avatar por outro a cada estado.
- Não o alterne com renderização condicional em estados curtos. Ocultá-lo com CSS ou pausá-lo preserva o arquivo carregado e a posição.
O guia de avatar de chat com streaming mostra um componente completo construído dessa forma.
O que os leitores de tela devem ouvir?
O canvas em si não transmite nada a um leitor de tela, e nem deveria tentar. Marque o contêiner do avatar com aria-hidden="true", como nos exemplos acima, para que as tecnologias assistivas o ignorem.
Em vez disso, coloque a informação em texto:
- Um rótulo de status visível, como "Ouvindo", "Pensando" ou "Falando", ao lado do avatar ou do campo de entrada. Ele também ajuda usuários videntes que não estão olhando para o avatar.
- Uma live region do tipo polite (
role="status") para esse rótulo, para que as mudanças sejam anunciadas sem interromper. - Anuncie apenas mudanças relevantes. O estado de pensando pode durar uma fração de segundo em respostas rápidas. Anunciar cada transição gera ruído; anunciar "Pensando" só depois de um pequeno atraso, e a própria resposta quando ela chegar, costuma ser suficiente.
- Não descreva a animação. "O robô está acenando" não acrescenta nada. "Conectado" acrescenta.
As reações de erro merecem atenção especial: a reação é decoração, mas o erro é informação. Sempre acompanhe a reação de uma mensagem em texto e evite efeitos que piscam. Veja animação acessível e movimento reduzido para as regras mais amplas da WCAG.

Torne um avatar de IA acessível e eficiente
- Oculte o canvas das tecnologias assistivas. Envolva o canvas do Rive em um contêiner com
aria-hidden="true"e mostre um status em texto em um elementorole="status". - Respeite o movimento reduzido. Verifique
prefers-reduced-motione, quando estiver definido comoreduce, renderize uma imagem estática da pose idle em vez do avatar animado. - Pause quando não estiver visível. Chame
rive.pause()quando um IntersectionObserver indicar que o avatar saiu da tela ou a página ficar oculta, erive.play()quando ele voltar. - Deixe o avatar em repouso após inatividade. Depois de um período sem conversa, mude para um estado sleeping ou idle e pause a renderização até a próxima mensagem.
- Mantenha uma única instância estável. Monte um único avatar animado fora da lista de mensagens, mantenha
useRiveeRiveComponentjuntos e deixeuseOffscreenRendererativado.
Perguntas frequentes
Um avatar de chatbot animado precisa de botão de pausa?
Se ele fica em loop por mais de cinco segundos junto com outros conteúdos e começa automaticamente, o critério 2.2.2 da WCAG exige uma forma de pausar, parar ou ocultar a animação. Um botão de alternância resolve. Voltar a uma pose estática poucos segundos depois que a conversa silencia também mantém curto o movimento idle, mas loops longos de fala ainda se beneficiam de um controle.
Devo pausar um avatar Rive quando ele sai da tela com a rolagem?
Sim. As boas práticas do Rive recomendam chamar pause quando um gráfico está fora da tela e não precisa continuar tocando. Use um IntersectionObserver para detectar a visibilidade e a Page Visibility API para abas em segundo plano, e chame rive.pause() e rive.play() conforme elas mudam.
O que um avatar deve mostrar com movimento reduzido?
Uma imagem estática de uma pose limpa, geralmente idle, com o estado atual mostrado em texto, como Ouvindo ou Pensando. Evite pausar a animação em um frame qualquer. Se o estado precisar continuar visível, troque entre algumas imagens estáticas em vez de animar a transição entre elas.
Posso mostrar vários avatares Rive em uma mesma página?
Sim. O runtime de React compartilha por padrão um único contexto WebGL offscreen entre as instâncias por meio do useOffscreenRenderer, o que evita os limites de contexto do navegador. Cada instância em reprodução ainda consome tempo de renderização, então, em um chat, anime apenas o avatar atual e use imagens estáticas para as mensagens antigas.
Respostas rápidas deste guia
Fontes
- 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