Programação Por Equipe DevBras

Capa do artigo: Animação que não para: como respeitar o "reduzir movimento" no CSS e no JavaScript

Animação que não para: como respeitar o "reduzir movimento" no CSS e no JavaScript

Detecte prefers-reduced-motion no CSS e no JavaScript, troque carrossel, parallax e vídeo em loop por versões paradas e confira tudo com o emulador do DevTools.

A home da loja tem um banner com gradiente que pulsa, um carrossel que troca de slide a cada quatro segundos e um fundo com parallax que desliza enquanto a pessoa rola. Para o time de marketing, é vida. Para uma cliente com labirintite, é enjoo em dez segundos, e ela fecha a aba antes de ver o preço. O detalhe que dói: ela já tinha avisado o sistema operacional que queria menos movimento. O site simplesmente não perguntou.

A resposta curta: leia a preferência prefers-reduced-motion e, quando ela vier como reduce, desligue toda animação contínua ou decorativa. Carrossel para, parallax vira fundo fixo, vídeo de fundo não toca sozinho. Transições curtas que explicam uma mudança de estado podem ficar, mais suaves. E nenhuma informação pode existir só dentro de uma animação.

De onde vem a preferência e por que o site precisa ouvir

Windows, macOS, iOS e Android têm uma opção de acessibilidade para reduzir ou remover animações. No Windows ela aparece como "Efeitos de animação"; no macOS e no iOS, como "Reduzir movimento"; no Android, como "Remover animações". O navegador lê essa escolha e a expõe para o CSS pela media query prefers-reduced-motion, com dois valores: no-preference e reduce.

Quem liga essa opção não está pedindo um site feio. São pessoas com distúrbios vestibulares, enxaqueca, epilepsia fotossensível, TDAH ou simplesmente quem lê no ônibus e não aguenta coisa se mexendo. Para elas, movimento em área grande (parallax, zoom, fundo que desliza) é o pior caso, porque o cérebro interpreta como deslocamento do próprio corpo.

Há também uma regra de acessibilidade por trás. As diretrizes WCAG pedem que conteúdo que se move sozinho, começa automaticamente e dura mais de cinco segundos tenha um jeito de pausar, parar ou esconder. Então o carrossel precisa de botão de pausa mesmo para quem não ligou preferência nenhuma. A media query é a camada extra: para quem já disse o que quer, nem começa.

Este guia trata da animação que não para. A discussão sobre qual transição de interface ajuda e qual atrapalha está em animação de interface que explica e animação que só distrai.

Como implementar a versão quieta, passo a passo

O exemplo é a home da loja: banner pulsando, carrossel automático, parallax e um vídeo de fundo em loop.

  1. Faça o inventário. Abra a página e anote tudo que se move sem a pessoa pedir: @keyframes com infinite, setInterval que troca slide, bibliotecas de scroll, vídeo com autoplay, GIF animado. Na loja deu quatro itens.
  2. Prefira o movimento como opção, não como padrão. Em vez de escrever a animação e depois desligá-la no reduce, escreva a versão parada e só adicione movimento dentro de no-preference. Navegador antigo que não entende a query fica com a versão quieta, que é a segura.
  3. Trate o CSS. Animações decorativas entram só com no-preference. O scroll-behavior: smooth também, porque rolagem suave longa incomoda.
  4. Trate o JavaScript. CSS não para um setInterval. Leia a mesma preferência com matchMedia e não inicie o autoplay quando ela for reduce.
  5. Ouça a mudança. A pessoa pode ligar a opção com a página aberta. Registre um ouvinte de change e pare o que estiver rodando.
  6. Dê controle a todos. Coloque um botão de pausa visível no carrossel, para quem não configurou nada no sistema mas quer ler em paz.

O CSS da loja

/* Versão padrão: parada */
.banner { background: linear-gradient(90deg, #0b3d91, #1565c0); }
.hero { background-image: url("/img/hero.jpg"); background-size: cover; }

@media (prefers-reduced-motion: no-preference) {
  html { scroll-behavior: smooth; }

  .banner {
    background-size: 200% 100%;
    animation: pulsar 6s ease-in-out infinite;
  }

  .hero { background-attachment: fixed; } /* parallax simples */

  .card { transition: transform 200ms ease-out; }
  .card:hover { transform: translateY(-4px); }
}

@keyframes pulsar {
  0%, 100% { background-position: 0% 50%; }
  50% { background-position: 100% 50%; }
}

Repare que a transição do card mora dentro do bloco também. Ela é curta e responde a uma ação, então você poderia mantê-la no reduce. Na loja ela é puro enfeite, então ficou de fora. A decisão é sua, desde que seja consciente.

O JavaScript do carrossel e do vídeo

const pedeMenos = window.matchMedia('(prefers-reduced-motion: reduce)');
const carrossel = document.querySelector('.carrossel');
const botaoPausa = carrossel.querySelector('.carrossel-pausa');
const video = document.querySelector('.video-fundo');
let timer = null;

function proximoSlide() {
  const atual = carrossel.querySelector('.slide.ativo');
  const proximo = atual.nextElementSibling || carrossel.querySelector('.slide');
  atual.classList.remove('ativo');
  proximo.classList.add('ativo');
}

function iniciar() {
  if (timer || pedeMenos.matches) return;
  timer = setInterval(proximoSlide, 4000);
  botaoPausa.textContent = 'Pausar';
}

function parar() {
  clearInterval(timer);
  timer = null;
  botaoPausa.textContent = 'Reproduzir';
}

function aplicarPreferencia() {
  if (pedeMenos.matches) {
    parar();
    video.pause();
    video.removeAttribute('autoplay');
  } else {
    iniciar();
  }
}

botaoPausa.addEventListener('click', () => (timer ? parar() : iniciar()));
pedeMenos.addEventListener('change', aplicarPreferencia);
aplicarPreferencia();

No HTML, o vídeo de fundo leva um poster com uma imagem parada que represente bem o conteúdo. Quando o vídeo não toca, é o pôster que aparece, e a página continua bonita. O mesmo vale para GIF animado: troque por uma imagem estática dentro de um <picture> com <source media="(prefers-reduced-motion: reduce)"> apontando para o PNG.

Um detalhe do código: se a pessoa clicar em "Reproduzir" com a preferência ligada, nada acontece, porque iniciar respeita a preferência. Se você quiser permitir que ela force o movimento, guarde a escolha explícita numa variável e dê prioridade a ela. O importante é que o padrão seja parado.

Como testar sem mudar o seu sistema

No Chrome e no Edge, abra o DevTools, vá ao painel Rendering (pelo menu de comandos, digitando "rendering") e use a opção de emular a media feature prefers-reduced-motion. O Firefox tem configuração equivalente em about:config, mas o caminho mais confiável para um teste real é ligar a opção do sistema operacional por cinco minutos e navegar a página inteira.

Com a emulação ligada, faça três verificações na loja: o banner parou de pulsar, o carrossel não avançou sozinho em 30 segundos e o vídeo mostra o pôster. Depois desligue a emulação com a página aberta e confira se o carrossel volta a girar sem recarregar. Isso prova que o ouvinte de change funciona.

Erros comuns

  • Zerar tudo com um seletor universal. O famoso * { animation: none !important; transition: none !important; } desliga até o spinner de carregamento e o feedback de um botão, e pode quebrar componentes que esperam o evento transitionend. A pessoa fica sem saber se o clique funcionou.
  • Tratar só o CSS. O carrossel controlado por JavaScript continua girando, e a biblioteca de parallax continua mexendo o fundo via transform. A media query sozinha não alcança o script.
  • Esconder informação na animação. O preço promocional só aparece no terceiro slide, que agora nunca chega. Com movimento reduzido, todos os slides precisam ser alcançáveis por botão ou estar visíveis em lista.
  • Carrossel sem botão de pausa. Quem não sabe da opção do sistema fica refém do autoplay, e a página falha a regra de pausar conteúdo que se move por mais de cinco segundos.
  • Testar só no carregamento. Sem o ouvinte de change, a pessoa liga a opção e precisa recarregar a página para ser atendida, o que ela raramente sabe que precisa fazer.
  • Trocar animação por piscar. Substituir o deslize por um "fade" rápido e repetitivo não é versão quieta. Pisca-pisca é justamente o que mais incomoda quem tem sensibilidade.

Checklist

  • Inventário feito: toda animação contínua, autoplay e GIF da página está listado.
  • Animações decorativas só existem dentro de @media (prefers-reduced-motion: no-preference).
  • O JavaScript consulta matchMedia('(prefers-reduced-motion: reduce)') antes de iniciar autoplay.
  • Existe ouvinte de change que para o movimento sem recarregar a página.
  • Vídeo de fundo tem poster e não toca sozinho com a preferência ligada.
  • Carrossel tem botão de pausa visível e alcançável por teclado.
  • Nenhum conteúdo importante depende de um slide ou animação chegar sozinho.
  • Teste feito com a emulação do DevTools e com a opção real do sistema operacional.

Ligue agora o "reduzir movimento" do seu sistema, abra a página mais visitada do seu site e espere 30 segundos sem tocar em nada: ainda tem alguma coisa pulando, girando ou deslizando sozinha?

Avalie este artigo

Referência: https://developer.mozilla.org/docs/Web/CSS/@media/prefers-reduced-motion