Animação acessível: movimento reduzido e WCAG
Por equipe Charmotion · Atualizado em

Uma animação acessível respeita a configuração de Reduzir movimento do usuário e pode ser pausada. Verifique prefers-reduced-motion: reduce na web ou accessibilityReduceMotion no SwiftUI e troque o movimento em loop por uma pose estática. Pelo SC 2.2.2 da WCAG 2.2, movimento automático que dura mais de cinco segundos ao lado de outro conteúdo precisa de um controle para pausar, parar ou ocultar.
Principais pontos
prefers-reduced-motiontem dois valores,no-preferenceereduce, e é compatível com todos os navegadores modernos.- Quando o valor for
reduce, substitua os loops do personagem por uma pose estática ou um fade curto. - WCAG 2.2.2 (Nível A): movimento com reprodução automática que dura mais de cinco segundos precisa de uma forma de pausar, parar ou ocultar.
- Animações de carregamento podem ser consideradas essenciais quando não é possível interagir durante a espera.
- Nunca faça nada piscar mais de três vezes por segundo (SC 2.3.1, Nível A).
O que é prefers-reduced-motion?
prefers-reduced-motion é uma media feature do CSS que informa à página se o usuário pediu ao sistema operacional para reduzir movimentos não essenciais. Ela tem dois valores: no-preference e reduce. Segundo a MDN, ela está disponível em todos os navegadores desde janeiro de 2020.
O usuário não ativa isso no navegador. Ele ativa no sistema operacional, e o navegador repassa a preferência:
- iOS e iPadOS: Ajustes > Acessibilidade > Movimento > Reduzir Movimento.
- macOS: Ajustes do Sistema > Acessibilidade > Tela > Reduzir movimento.
- Windows 11: Configurações > Acessibilidade > Efeitos visuais > Efeitos de animação (desativado).
- Android 9+: Configurações > Acessibilidade > Remover animações.
As pessoas ativam essa opção por motivos diferentes. Em quem tem distúrbios vestibulares, movimentos amplos, parallax e zoom podem causar tontura ou náusea. Outras pessoas acham o movimento constante uma distração durante a leitura. "Reduzir" não precisa significar "nenhum". Um fade suave de opacidade ou uma mudança de cor geralmente não causa problema. O que deve sair são deslocamentos amplos, giros, quiques e movimentos em loop que nunca param.
Como dar suporte a movimento reduzido em CSS e JavaScript?
No CSS, escreva a animação completa por padrão e desative-a dentro da media query. Uma alternativa mais segura é envolver a animação em @media (prefers-reduced-motion: no-preference), assim o movimento só é adicionado quando o usuário não tem preferência. O primeiro padrão fica assim:
.mascot { animation: bob 3s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.mascot { animation: none; }
}
No JavaScript, use matchMedia. Escute também as mudanças, porque o usuário pode alterar a configuração com a página aberta:
const query = window.matchMedia("(prefers-reduced-motion: reduce)");
function applyMotionPreference() {
document.documentElement.classList.toggle("reduce-motion", query.matches);
}
applyMotionPreference();
query.addEventListener("change", applyMotionPreference);
No React, coloque isso em um pequeno hook que retorna query.matches e renderiza de novo quando o valor muda; depois, use esse valor como condição sempre que iniciar uma animação.
Em loops de vídeo, verifique a preferência antes de reproduzir. Se for reduce, deixe o <video> pausado no frame de pôster ou renderize a imagem de pôster no lugar do vídeo.
Como trocar um loop do Rive ou de vídeo por uma pose estática?
O padrão mais simples, que funciona com qualquer formato: tenha uma imagem estática da pose neutra do personagem e renderize essa imagem no lugar da animação quando o movimento reduzido estiver ativado.
Em um vídeo, essa imagem é o frame de pôster. Exporte o primeiro frame do loop idle como PNG ou WebP transparente, assim a versão com movimento reduzido fica idêntica, só que parada.
Para um personagem em Rive, você tem duas opções:
- Renderizar uma imagem estática da pose idle em vez de montar o canvas do Rive. Isso também evita carregar o runtime, o que economiza banda para esses usuários.
- Manter o Rive, mas fixar um estado calmo. Coloque a state machine no loop mais calmo e pule as reações únicas que quicam ou deslizam. As state machines do Rive podem "assentar", ou seja, o runtime para de avançar quando nada muda.
Se você usa um arquivo do Charmotion, os loops idle (mood 0) e sleeping (mood 8) são os estados mais calmos para manter. Seja qual for o caminho escolhido, continue transmitindo a informação que o movimento passava. Se uma animação de sucesso avisa que um pagamento foi aprovado, mantenha o texto ou o ícone que diz isso.
Como dar suporte ao Reduzir Movimento no iOS?
O SwiftUI expõe a configuração do usuário pelo valor de ambiente accessibilityReduceMotion. Leia esse valor em qualquer view e escolha o caminho estático quando ele for verdadeiro:
struct MascotView: View {
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
if reduceMotion {
Image("mascot-idle-still")
.resizable()
.scaledToFit()
} else {
AnimatedMascot() // your Rive view or video player
}
}
}
O valor é atualizado em tempo real, então a view muda se o usuário alterar os Ajustes enquanto o app estiver aberto. Aplique a mesma regra às transições que você mesmo adicionar: troque um deslize ou um efeito de mola por um cross-fade, ou simplesmente remova a animação.
Na web, o Safari no iOS informa a mesma configuração por meio de prefers-reduced-motion, então uma única opção do sistema vale para o seu app e para o seu site.
Meu mascote em loop precisa de um botão de pausa?
Muitas vezes, sim. O Critério de Sucesso 2.2.2 Pausar, Parar, Ocultar da WCAG 2.2 é de Nível A, o mínimo exigido pela maioria das políticas de acessibilidade. Ele se aplica a conteúdo em movimento, piscante ou com rolagem que atenda às três condições:
- Começa automaticamente.
- Dura mais de cinco segundos.
- É exibido ao mesmo tempo que outros conteúdos.
Se as três forem verdadeiras, o usuário precisa de um mecanismo para pausar, parar ou ocultar o conteúdo, a menos que o movimento seja essencial. Um mascote em idle que fica em loop infinito ao lado do texto da página atende às três.
Formas de cumprir o critério:
- Pare sozinho. Toque o loop por alguns ciclos, com menos de cinco segundos no total, e depois mantenha um frame estático.
- Adicione um controle visível. Um botão de pausa perto da animação que chame
video.pause()ou troque o canvas do Rive pela imagem estática. - Deixe o usuário ocultar. Um controle para dispensar remove o movimento por completo.
Respeitar prefers-reduced-motion é uma boa prática, mas não cumpre o 2.2.2 sozinho. Muitas pessoas que se distraem com movimento nunca encontraram essa configuração do sistema.
Animações de carregamento são exceção?
Podem ser. O documento Understanding do W3C para o SC 2.2.2 diz que uma animação exibida durante uma fase de pré-carregamento "pode ser considerada essencial se a interação não puder ocorrer durante essa fase para todos os usuários". O raciocínio: esconder o único sinal de progresso poderia fazer o usuário achar que o app travou.
Essa exceção é restrita. Ela cobre uma tela de carregamento em tela cheia ou uma espera que bloqueia o uso. Não cobre um mascote que mantém um loop de loading ou thinking rodando num canto enquanto o usuário ainda pode ler e digitar. Nesse caso, mantenha o loop discreto e pare-o quando a tarefa terminar, ou ofereça uma pausa.
Vincule os loops de carregamento a um trabalho real. Inicie o loop quando a requisição começar e encerre-o com uma reação única de sucesso ou erro quando ela terminar. Um loop que dura mais que a tarefa é, ao mesmo tempo, um problema de acessibilidade e um sinal de status enganoso. Para orientações de tempo em esperas, veja Duração de animações e UX de carregamento.
E as animações de interação e os flashes?
Mais dois critérios se aplicam à animação de personagens.
SC 2.3.3 Animação a partir de interações (Nível AAA)
"A animação de movimento disparada por interação pode ser desativada, a menos que a animação seja essencial." Isso inclui, por exemplo, um mascote que pula quando um botão é tocado ou um card que atravessa a tela. O Nível AAA raramente é obrigatório, mas respeitar prefers-reduced-motion nessas reações atende ao critério com pouco custo. Mantenha o feedback; só transforme-o em um fade, uma mudança de cor ou uma imagem estática em vez de movimento.
SC 2.3.1 Três flashes ou abaixo do limite (Nível A)
O conteúdo não pode piscar mais de três vezes em qualquer período de um segundo, a menos que fique abaixo dos limites gerais e de flash vermelho. Flashes rápidos podem provocar crises em pessoas com epilepsia fotossensível. Fique atento a isso nas animações de erro e de atenção: um piscar vermelho ou um brilho estroboscópico é exatamente onde o problema aparece. Mantenha as piscadas lentas e suaves.
Mascotes na interface de produto raramente precisam piscar. Comunique urgência com um único pulso ou uma mensagem clara.
Como deixar uma animação de personagem em loop acessível
- Prepare uma pose estática. Exporte um PNG ou WebP com fundo transparente do frame neutro ou idle do personagem.
- Detecte a preferência. Use
matchMedia("(prefers-reduced-motion: reduce)")na web ouaccessibilityReduceMotionno SwiftUI, e observe as mudanças. - Troque o movimento pela imagem estática. Com o movimento reduzido ativado, renderize a imagem estática em vez do canvas do Rive ou do vídeo, ou mantenha um estado calmo sem reações únicas.
- Adicione um controle de pausa. Para loops que começam automaticamente e duram mais de cinco segundos ao lado de outros conteúdos, adicione um botão visível de pausar, parar ou ocultar (WCAG 2.2.2).
- Limite os loops de carregamento. Inicie os loops de carregamento quando o trabalho começar e encerre-os com uma reação de sucesso ou erro quando ele terminar.
- Verifique os flashes. Garanta que nada pisque mais de três vezes por segundo, principalmente nos estados de erro e de atenção.
Perguntas frequentes
prefers-reduced-motion significa que devo remover toda a animação?
Não. Reduza movimentos grandes, parallax, giros, quiques e loops infinitos. Fades suaves de opacidade, mudanças de cor e uma pose estática do personagem geralmente não têm problema. Preserve a informação que o movimento transmitia, como um estado de sucesso ou erro, por meio de texto, ícones ou uma imagem estática.
Respeitar prefers-reduced-motion basta para a WCAG?
Sozinho, não. É a forma usual de atender ao SC 2.3.3 (AAA), mas o SC 2.2.2 (Nível A) ainda exige um mecanismo de pausar, parar ou ocultar para movimento com reprodução automática de mais de cinco segundos exibido junto a outros conteúdos, porque muitos usuários nunca alteram a configuração do sistema.
Como testo o movimento reduzido?
Ative Reduzir Movimento nos ajustes de acessibilidade do iOS ou do macOS, desative os Efeitos de animação no Windows 11 ou ative Remover animações no Android. No Chrome DevTools, você também pode emular o recurso de mídia pelo painel Rendering. Depois, confirme que os loops param e que a pose estática aparece.
Respostas rápidas deste guia
Fontes
- prefers-reduced-motion, MDN Web Docs
- accessibilityReduceMotion, Apple Developer Documentation
- Understanding SC 2.2.2: Pause, Stop, Hide, W3C WAI
- Understanding SC 2.3.3: Animation from Interactions, W3C WAI
- Understanding SC 2.3.1: Three Flashes or Below Threshold, W3C WAI
- State machines (web runtime), Rive