Skip to content

A11y/Motion: prefers-reduced-motion não alcança nenhuma animação de framer-motion — countdown em tela cheia e pista da corrida ignoram a preferência (§VII.4) #108

Open

Description

@caioross

O problema

docs/UI-AAA-OVERHAUL.md §VII.4 trata prefers-reduced-motion como métrica binária de "AAA pronto":

  • prefers-reduced-motion honrado em cada efeito; sem flash > 3Hz.

Hoje o projeto tem um fallback global em globals.css:49-56:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.001ms !important;
    transition-duration: 0.001ms !important;
    ...
  }
}

Essa regra cobre só CSS (animation e transition). Framer Motion não passa por nenhum dos dois: ele roda um laço de requestAnimationFrame e escreve style.transform / style.opacity inline a cada quadro. Zerar transition-duration não tem efeito sobre um valor que já chega interpolado no estilo inline. Ou seja: o usuário liga "reduzir movimento" no sistema, o CSS obedece, e todo o movimento de framer-motion continua exatamente igual.

Por isso vários arquivos comentam confiando na regra global — ex. CodeEditor.tsx:85 ("sob prefers-reduced-motion a regra global zera a duração"). Para aquele caso o comentário está certo, porque o shake é uma animation CSS. Para os <motion.*> abaixo, não está.

Inventário (medido em origin/main @ 82f7817)

10 componentes importam framer-motion e não importam useReducedMotion:

Componente Movimento Gravidade
Countdown.tsx:11-18 dígito de 160–220px em tela cheia: scale 0.4→1 + rotate -8°→0, saída scale 1.6, 1× por segundo, 3×seguidas 🔴 pior caso
RaceTrack.tsx:142-156 2 springs por jogador (barra + racer) animando continuamente durante a corrida 🔴 área sagrada
Modal.tsx:30-41 · Toast.tsx:25-29 slide + scale de entrada/saída 🟠
HomeView.tsx (11) · Lobby.tsx (13) · RoomView · Chat · FloatingChat · Logo reveals de entrada 🟡

O Countdown é o mais sério e a spec já o marca nominalmente (§ linha 552): "O flash branco respeita reduced-motion (vira mudança de cor suave) — importante para fotossensibilidade". Hoje não respeita nada: é o maior elemento da tela pulsando 3× seguidas, sem escapatória, e é inevitável — passa em toda partida multiplayer.

O RaceTrack é o segundo porque não é um reveal que passa: são springs vivos ao lado da textarea durante a corrida inteira. Com o limite de 30 jogadores (room.ts:282, #69) são até 60 springs simultâneos.

Que o padrão certo já existe no repo é o que torna isto barato: Results, TypingCore, SpectatorView, AuroraBackground, ClickSpark, DecryptedText e o BannerCarousel novo (:100) já usam useReducedMotion() corretamente. Isto é propagar um padrão da casa, não inventá-lo.

Escopo desta fatia

Só os dois de gravidade 🔴 + a primitiva compartilhada. Os 8 restantes são mecânicos e viram fatia 2 (declarada abaixo) — a intenção é estabelecer o padrão com revisão cuidadosa antes de espalhá-lo por 8 arquivos.

Acceptance criteria

  • Existe uma primitiva única (sugestão: src/lib/motion.ts, que a §VII.1 Fase 0 já prevê) exportando os presets a serem reusados — sem repetir const reduced = useReducedMotion() + ternário solto em cada arquivo.
  • Countdown: com prefers-reduced-motion: reduce, o dígito não escala nem rotaciona. Troca permitida: cross-fade de opacidade, ou nenhuma transição. O key={n} e o AnimatePresence continuam trocando o número a cada segundo (a informação não pode sumir).
  • RaceTrack: com reduced-motion, barra e racer vão para a posição nova sem spring (transition={{ duration: 0 }} ou equivalente). Sem salto de layout e sem perder a posição correta.
  • Nenhum dos dois perde informação sob reduced-motion: o número do countdown continua legível a cada tick e o progresso de cada jogador continua correto.
  • O comentário enganoso de CodeEditor.tsx:85 fica; mas não adicione comentários novos afirmando que a regra global do CSS cobre framer-motion.
  • Gate: pnpm typecheck + pnpm build + vitest + validate-metrics (o RaceTrack é vizinho da área sagrada).

Verificação sugerida (leia antes de tentar screenshot)

.claude/skills/cr-fleet-ops §11 e a memória da frota registram que no Browser pane headless o rAF congela — framer-motion trava em opacity: 0 e o visual mente. Não tente provar isto por screenshot.

O que funciona: emular a media query e instrumentar a lógica — ler o valor que o componente passa em transition sob cada preferência, ou um teste de render (jsdom + matchMedia mockado) afirmando que transition.duration === 0 / ausência de scale no initial quando reduce está ligado. Um teste assim também protege contra regressão, que é o que a §VII.4 exige de uma métrica binária.

Fora de escopo (fatia 2 — abrir depois desta mergear)

Os 8 componentes 🟠/🟡 (Modal, Toast, HomeView, Lobby, RoomView, Chat, FloatingChat, Logo), reusando a primitiva criada aqui.

Não confundir com a #59

A #59 fatia 2 também toca o RaceTrack, mas por performance (tirar a pista do caminho do re-render + coalescing). Reduced-motion foi deixado explicitamente de fora dela em 28/07 e é o que esta issue cobre. Se as duas forem pegas juntas, atenção ao conflito no mesmo arquivo.

Activity

  1. added
    P2Prioridade normal
    area:uiInterface, design system, animação, game feel
    on Jul 29, 2026
  2. caioross commented on Jul 31, 2026

    @caioross
    OwnerAuthor

    🗂️ Correção pontual do inventário (Curador · 2026-07-31)

    O inventário desta issue lista o TypingCore entre os componentes que "já usam useReducedMotion() corretamente". Usa — mas só no IdleWarning. A barra de progresso do card de estatísticas (TypingCore.tsx:251-256) anima num spring sem consultar o reduced que já existe na linha 60.

    Não há nada a fazer aqui: essas 5 linhas serão reescritas pela #114 (a barra anima width — mesmo defeito da #59), e o guard de reduced-motion entra junto, como AC dela. Registro só para que a fatia 2 não conte com um inventário incompleto nem volte a este arquivo por engano.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Prioridade normalarea:uiInterface, design system, animação, game feel

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions