Communitygithub.com

jo o6077/Reação Animação

Habilita animações React por meio de CSS, View Transitions e Pointer Events, sem bibliotecas de terceiros, garantindo boas práticas de acessibilidade e desempenho.

O que é Reação Animação?

Este skill é um guia completo para implementar animações em projetos React, seguindo uma rígida política de **zero bibliotecas** de animação externas (como Framer Motion ou GSAP). Ele organiza as animações em três níveis de complexidade (Tiers): **Tier 1** usa utilitários CSS do Tailwind e `@keyframes` para animações simples (hover, fade-in, scroll-driven); **Tier 2** emprega a View Transitions API (com wrappers `withViewTransition` e o componente `<ViewTransition>` do React 19.3) para transições de rota e elementos compartilhados; **Tier 3** implementa gestos complexos (drag-and-drop) usando Pointer Events e uma máquina de estados finita (FSM). O documento é extremamente detalhado, com dezenas de "Gotchas" que previnem erros comuns, como a falta de tratamento de `pointercancel`, a ausência de `prefers-reduced-motion`, ou a duplicação de nomes em `view-transition-name`. Ele também inclui um processo de verificação de evidências (render-evidence-protocol.md) para garantir que as animações realmente funcionem nos navegadores, evitando entregas não verificadas. Suporta ambientes React, Next.js e shadcn, e é compatível com navegadores modernos, com fallbacks para os mais antigos.

Funciona com✓Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/joo6077/claude-plugins/tree/HEAD/react-kit/skills/react-animation

Perguntar na sua IA favorita

Abre um novo chat com esta habilidade de agente já pré-carregada.

Documentação

O que Reação Animação faz?

A versão instalada não possui docs/react/ — se o caminho docs/... deste arquivo não puder ser aberto, tente ler adicionando o mesmo caminho após https://raw.githubusercontent.com/joo6077/claude-plugins/main/. Se ainda assim não for possível, não invente conteúdo; apenas registre que não foi possível ler.

Pegadinhas

  1. Princípio de zero bibliotecas — sem exceções: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css são proibidos de instalar e importar. Se alguma importação dessas existir na base de código, /react-audit emitirá uma falha de build. Mesmo que o usuário solicite, ofereça uma implementação alternativa e recuse o uso de bibliotecas.

  2. Comece sempre pelo nível mais baixo: Ao receber uma solicitação de "animação", é comum pular para o Tier 3. Siga as regras de decisão: verifique primeiro se o Tier 1 resolve. Só avance para tiers superiores quando for estritamente necessário.

  3. Verifique o plugin tailwindcss-animate: Utilitários como animate-in, fade-in-*, slide-in-from-* exigem o tailwindcss-animate. Projetos com shadcn já o incluem por padrão, mas, se estiver ausente, execute pnpm add -D tailwindcss-animate e registre o plugin no tailwind.config.ts.

  4. Unicidade do view-transition-name: Dois elementos não podem ter o mesmo nome simultaneamente, senão ocorre o erro "duplicate name". Use IDs dinâmicos (ex.: view-transition-name: task-${item.id}) para garantir a unicidade.

  5. Tratamento obrigatório de pointercancel: Ignorar o evento pointercancel pode causar vazamento de estado de arrasto durante troca de abas, foco em outro aplicativo ou desmontagem de elemento. No manipulador onPointerCancel, redefina a FSM para idle.

  6. touch-action: none é essencial para arrasto em dispositivos móveis: Em elementos com arrasto baseado em pointer events, adicione a classe Tailwind touch-none para bloquear rolagem e zoom padrão do navegador.

  7. Store de arrasto do Zustand é uma instância global única: Não crie stores de arrasto separadas por funcionalidade. Use src/presentation/shared/stores/drag-store.ts como única store compartilhada por todas as funcionalidades.

  8. prefers-reduced-motion deve ser respeitado: Em todos os tiers, desabilite animações com @media (prefers-reduced-motion: reduce) ou com a variante motion-reduce:* do Tailwind. O /react-audit emitirá um aviso se estiver ausente.

  9. Animações são exclusivas da camada de apresentação: As camadas domain/ e data/ não podem importar pointer events, view transitions ou a store de arrasto do Zustand. Código de animação deve estar apenas em src/presentation/.

  10. Firefox não suporta o parâmetro types: A opção types em document.startViewTransition({ update, types: [...] }) é suportada de forma estável apenas em navegadores Chromium (a partir de 2026). Use try/catch no wrapper withViewTransition para fallback.

  11. Suporte a scroll-driven animation (atualizado em 2026-08-13): animation-timeline: scroll() / scroll-timeline tem 85,43% de cobertura global (Can I Use) — Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+. A afirmação de que o Firefox está atrás de uma flag está desatualizada. No entanto, view() ainda não é Baseline segundo o MDN. Portanto, mantenha a política de usar @supports (animation-timeline: scroll()) / @supports (animation-timeline: view()) com fallback para estilos estáticos.

  12. Uso de view-transition-name: match-element (Chrome 137+): Em vez de atribuir nomes únicos manualmente a dezenas de elementos, use o valor CSS view-transition-name: match-element para que o navegador faça a correspondência automaticamente. No entanto, é exclusivo do Chromium (2026-Q2), então mantenha uma rota de fallback.

  13. Movimento não é comprovado por código estático — evidência obrigatória antes de declarar conclusão (E2): O resultado deste skill só pode ser verificado quando reproduzido. O fato de classes animate-* estarem presentes ou de o código chamar startViewTransition é evidência estática (R3), não prova que a animação realmente foi executada — keyframes indefinidos, prefers-reduced-motion sempre ativo, clipping por overflow: hidden no pai, ou entrada no fallback de navegadores não suportados podem silenciosamente resultar em "nada acontece". Antes de finalizar, preencha a checklist da seção §4 de react-kit/references/render-evidence-protocol.md. Ao verificar com comparação de snapshots, aponte dois momentos (antes/depois) — um único frame estático não é um oráculo para qualquer movimento (§3 d). Se não for possível obter evidência, adicione [Não Verificado] e preencha os quatro campos (o que impede, tentativas de contorno, motivo fora de controle, comando para re-verificação — conforme §2 do protocolo) e reporte como conclusão parcial.

    Não introduza bibliotecas de animação para obter evidências. A Política de Bibliotecas (Pegadinha #1) está acima deste protocolo e não é flexibilizada por nenhuma conveniência de verificação.

  14. Capture a referência antes de editar — Execute o Passo 0 da §1 e o primeiro item da sequência de comparação da §2 de react-kit/references/render-evidence-protocol.md antes da primeira edição. Registre o resultado (paráfrase, tabela de convenções, caminho do instantâneo do estado atual e marcações do que precisa mudar) na resposta. Depois de editar, não é possível capturar o estado anterior novamente.

  15. Tier 2 é dividido pelo tipo de atualização — <ViewTransition> do React 19.3 é API estável (adicionado em 2026-09-26): O <ViewTransition> importado de react tornou-se uma API estável no React 19.3, não mais experimental. Ele é executado em atualizações marcadas como Transition (atualizações dentro de startTransition, Suspense reveal, useDeferredValue) quando o estilo muda ou ocorre montagem/desmontagem, e funciona apenas no DOM (fonte: https://react.dev/blog/2026/09/09/react-19-3 «We shared it as an experimental API last year, and in 19.3 it's stable and ready to use.»). Portanto, para atualizações de estado React marcadas como Transition, use <ViewTransition>; para outras atualizações DOM (atualizações síncronas não-Transition, manipulação DOM fora do React), mantenha o wrapper withViewTransition da §3.1. Não remova o wrapper. Como é um componente do pacote react, não viola o princípio de zero bibliotecas (#1). Se a versão do React no projeto for inferior a 19.3, use apenas o wrapper.

Processo

1. Decisão Automática de Nível

Se a flag --tier não estiver presente, analise a solicitação do usuário e escolha o nível mais baixo.

Palavra-chave / CenárioNível Decidido
"fade in", "slide up", "scale", "pulse", "bounce", "efeito hover", "opacity", "shimmer", "animação de entrada"T1
"transição de carregamento de skeleton → conclusão", "hover em botão", "accordion", "modal abrir/fechar", "mudança de estado"T1
"animação de scroll", "scroll-driven", "parallax", "barra de progresso de scroll", "baseado em scroll"T1
"transição de página", "elemento compartilhado", "grid para board", "transição de visualização", "transição de rota", "mudança de estrutura DOM"T2
"scroll-triggered", "disparo ao cruzar scroll", "iniciar animação em posição específica"T2
"arrastar e soltar", "lista ordenável", "sortable", "kanban", "arrastar", "soltar", "gesto", "linhas de conexão com setas"T3

Se o limite entre T2/T3 estiver ambíguo, consulte o agente animation-architect-react.

Resumo dos 3 Níveis:

NívelFerramentaCenário de AplicaçãoDificuldade
T1Utilitários Tailwind animate-* + CSS @keyframes + scroll-drivenMudança de estado, hover, loop simples, scrollBaixa
T2View Transitions API (wrapper withViewTransition) · React 19.3 <ViewTransition> (apenas atualizações marcadas como Transition — Pegadinha 15)Transição de visualização/rota, elemento compartilhado, FLIPMédia
T3Pointer Events + FSM + requestAnimationFrameArrastar e soltar, gestos, linhas de conexão SVGAlta

2. Nível 1 — Implementação com Tailwind + CSS

2.1 Utilitários Internos do Tailwind

CategoriaUtilitárioExemplo
Transiçãotransition-transform, transition-colors, transition-opacityhover:scale-105 transition-transform duration-200
Duraçãoduration-75~duration-1000duration-300
Easingease-linear, ease-in, ease-out, ease-in-outease-in-out
Animação (interna)animate-spin, animate-ping, animate-pulse, animate-bounceCarregamento, notificação
Atrasodelay-75~delay-1000Efeito de stagger

2.2 Utilitários Estendidos do tailwindcss-animate

animações de entrada/saída fornecidas pelo plugin tailwindcss-animate:

  • animate-in / animate-out
  • fade-in-*, fade-out-*
  • slide-in-from-top/bottom/left/right-*, slide-out-to-*
  • zoom-in-*, zoom-out-*

Combine com o seletor de atributo data-[state=...] para implementar animações baseadas em estado:

// Exemplo de drawer abrir/fechar
<div
  data-state={open ? 'open' : 'closed'}
  className="
    data-[state=open]:animate-in data-[state=closed]:animate-out
    data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0
    data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left
    duration-300
    motion-reduce:transition-none motion-reduce:animate-none
  "
>
  Conteúdo do Drawer
</div>

2.3 Declaração Direta de CSS @keyframes

Para animações personalizadas não cobertas pelos utilitários do Tailwind, declare no bloco @theme do globals.css:

/* src/presentation/styles/globals.css */
@import "tailwindcss";

@theme {
  --animate-wiggle: wiggle 1s ease-in-out infinite;
  --animate-shimmer: shimmer 2s linear infinite;

  @keyframes wiggle {
    0%, 100% { transform: rotate(-3deg); }
    50%      { transform: rotate(3deg); }
  }

  @keyframes shimmer {
    from { background-position: -200% 0; }
    to   { background-position: 200% 0; }
  }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

Uso: <div className="animate-wiggle">...</div>

2.4 Exemplos de Cenários do Nível 1

Entrada com fade-in:

<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
  Entrada com fade ao carregar a página
</div>

Botão com hover scale:

<button className="transition-transform duration-200 hover:scale-105 active:scale-95 motion-reduce:transition-none">
  Botão
</button>

Entrada em lista com stagger:

{items.map((item, i) => (
  <div
    key={item.id}
    className="animate-in slide-in-from-bottom-4 fade-in-0 motion-reduce:animate-none"
    style={{ animationDelay: `${i * 50}ms`, animationFillMode: 'backwards' }}
  >
    {item.name}
  </div>
))}

2.5 Animações Scroll-driven (CSS nativo)

Implemente animações vinculadas à posição de scroll usando CSS animation-timeline, sem JavaScript. Em 2026-08-13, scroll() / scroll-timeline é suportado no Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ (85,43% global segundo Can I Use), enquanto view() ainda não é Baseline pelo MDN. Em ambos os casos, envolva com @supports para fallback gracioso em navegadores não suportados.

Barra de progresso de scroll:

/* globals.css */
@keyframes progress-grow {
  from { transform: scaleX(0); }
  to   { transform: scaleX(1); }
}
<div
  className="fixed top-0 left-0 h-1 w-full origin-left bg-primary motion-reduce:hidden"
  style={{
    animation: 'progress-grow linear',
    animationTimeline: 'scroll(root block)',
  }}
/>

Fade-in ao entrar na tela (view timeline):

@keyframes fade-slide-in {
  from { opacity: 0; transform: translateY(2rem); }
  to   { opacity: 1; transform: translateY(0); }
}

@supports (animation-timeline: view()) {
  .scroll-reveal {
    animation: fade-slide-in linear both;
    animation-timeline: view();
    animation-range: entry 0% entry 100%;
  }
}
<section className="scroll-reveal motion-reduce:opacity-100">
  Conteúdo que aparece ao scrollar
</section>

Atenção: Animações scroll-triggered (Chrome 145, 2026) são um conceito diferente que dispara animações baseadas no tempo ao cruzar um deslocamento de scroll específico. Não confunda com scroll-driven. Scroll-triggered é classificado como candidato ao Nível 2.

3. Nível 2 — Implementação com View Transitions API

View Transitions no mesmo documento é Baseline 2025 pelo MDN, com 90,2% de cobertura global pelo Can I Use — Chrome/Edge 111+, Safari 18+, Firefox 144+ (atualizado em 2026-08-13). Fornece animação FLIP automática para mudanças no DOM de um SPA.

Por outro lado, a transição cross-document / MPA baseada em @view-transition e alguns recursos detalhados ainda têm disponibilidade limitada. Ao usar para transições de rota, mantenha o caminho same-document como padrão com feature detection e fallback (§6, lacuna de cobertura padrão nº 8).

3.1 Criação do Wrapper withViewTransition

Crie o arquivo se não existir:

// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
  updateDOM: () => void,
  options?: { types?: string[] },
): void {
  // Respeita prefers-reduced-motion
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    updateDOM()
    return
  }
  // Feature detection (fallback para navegadores antigos)
  if (typeof document.startViewTransition !== 'function') {
    updateDOM()
    return
  }
  // Tratamento de suporte ao parâmetro types (Firefox não suporta na implementação inicial)
  try {
    if (options?.types && options.types.length > 0) {
      document.startViewTransition({ update: updateDOM, types: options.types })
    } else {
      document.startViewTransition(updateDOM)
    }
  } catch {
    updateDOM()
  }
}

3.2 Elementos Compartilhados com view-transition-name no CSS

Mesmo que a estrutura DOM mude, identifique como "mesmo elemento" para uma transição suave:

// Garanta unicidade com ID dinâmico (nomes duplicados são proibidos)
<div
  className="rounded-lg border p-4"
  style={{ viewTransitionName: `task-${task.id}` }}
>
  {task.title}
</div>

3.3 Transição de Visualização Grid ↔ Board (Cenário Principal)

// src/presentation/features/tasks/components/task-switcher.tsx
import { withViewTransition } from '@/presentation/shared/lib/view-transition'
import { useTaskStore } from '../store'

export function TaskViewSwitcher() {
  const viewMode = useTaskStore((s) => s.viewMode)
  const setViewMode = useTaskStore((s) => s.setViewMode)

  return (
    <button
      onClick={() => {
        withViewTransition(() => {
          setViewMode(viewMode === 'grid' ? 'board' : 'grid')
        })
      }}
    >
      {viewMode === 'grid' ? 'Para visualização em board' : 'Para visualização em grid'}
    </button>
  )
}

export function TaskList({ tasks }: { tasks: Task[] }) {
  const viewMode = useTaskStore((s) => s.viewMode)
  return (
    <div className={viewMode === 'grid' ? 'grid grid-cols-3 gap-4' : 'flex gap-4 overflow-x-auto'}>
      {tasks.map((task) => (
        <div
          key={task.id}
          className="rounded-lg border p-4"
          style={{ viewTransitionName: `task-${task.id}` }}
        >
          {task.title}
        </div>
      ))}
    </div>
  )
}

3.4 Ajustes Finos na Transição CSS

/* globals.css — duração da transição + easing */
::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Ajuste individual para elementos específicos */
::view-transition-group(hero-image) {
  animation-duration: 0.6s;
}

/* Transição instantânea com reduced-motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

4. Nível 3 — Implementação de Primitivas de Pointer Personalizadas

Implemente diretamente com Pointer Events API + máquina de estados finita (FSM), sem bibliotecas.

4.1 Criação do Hook useDrag

// src/presentation/shared/hooks/use-drag.ts
import { useCallback, useEffect, useReducer, useRef } from 'react'

// FSM 3 estados: idle / dragging / dropping
type DragState =
  | { kind: 'idle' }
  | { kind: 'dragging'; startX: number; startY: number; offsetX: number; offsetY: number; pointerId: number }
  | { kind: 'dropping'; offsetX: number; offsetY: number }

type DragAction =
  | { type: 'start'; x: number; y: number; pointerId: number }
  | { type: 'move'; x: number; y: number }
  | { type: 'drop' }
  | { type: 'resolved' }
  | { type: 'cancel' }

function dragReducer(state: DragState, action: DragAction): DragState {
  switch (action.type) {
    case 'start':
      return { kind: 'dragging', startX: action.x, startY: action.y, offsetX: 0, offsetY: 0, pointerId: action.pointerId }
    case 'move':
      if (state.kind !== 'dragging') return state
      return { ...state, offsetX: action.x - state.startX, offsetY: action.y - state.startY }
    case 'drop':
      if (state.kind !== 'dragging') return state
      return { kind: 'dropping', offsetX: state.offsetX, offsetY: state.offsetY }
    case 'resolved':
    case 'cancel':
      return { kind: 'idle' }
  }
}

type DragHandlers = {
  onPointerDown: (e: React.PointerEvent) => void
  onPointerMove: (e: React.PointerEvent) => void
  onPointerUp: (e: React.PointerEvent) => void
  onPointerCancel: (e: React.PointerEvent) => void
}

export function useDrag(): {
  state: DragState
  handlers: DragHandlers
  ref: React.RefObject<HTMLElement | null>
  resolve: () => void
} {
  const [state, dispatch] = useReducer(dragR
...
}

4.2 Uso do useDrag

// src/presentation/features/tasks/components/draggable-task-card.tsx
import { useCallback, useEffect } from 'react'
import { useDrag } from '@/presentation/shared/hooks/use-drag'
import { useDragStore } from '@/presentation/shared/stores/drag-store'

export function DraggableTaskCard({ task }: { task: Task }) {
  const { state, handlers, ref, resolve } = useDrag()
  const { setDragState } = useDragStore()

  useEffect(() => {
    if (state.kind === 'dragging' || state.kind === 'dropping') {
      setDragState(state)
    }
  }, [state, setDragState])

  return (
    <div
      ref={ref}
      className="touch-none select-none rounded-lg border p-4 transition-shadow"
      style={{
        transform: state.kind === 'dragging'
          ? `translate(${state.offsetX}px, ${state.offsetY}px)`
          : 'translate(0, 0)',
        opacity: state.kind === 'dragging' ? 0.8 : 1,
        zIndex: state.kind === 'dragging' ? 50 : 'auto',
      }}
      {...handlers}
    >
      {task.title}
    </div>
  )
}

4.3 Gerenciamento de Arrasto com Drop Zone (Exemplo de Lista Ordenável)

Use a store global de arrasto para coordenar a zona de soltar:

// src/presentation/features/tasks/components/task-list.tsx
import { useDragStore } from '@/presentation/shared/stores/drag-store'
import { useTaskStore } from '../store'

export function TaskList({ tasks }: { tasks: Task[] }) {
  const setTaskPosition = useTaskStore((s) => s.setTaskPosition)
  const dragState = useDragStore((s) => s.state)

  const handleDrop = useCallback(
    (targetId: string) => {
      if (dragState.kind === 'dropping') {
        setTaskPosition(dragState.itemId, targetId)
        useDragStore.getState().resolve()
      }
    },
    [dragState, setTaskPosition],
  )

  return (
    <div className="space-y-2">
      {tasks.map((task) => (
        <div
          key={task.id}
          className={`rounded-lg border p-4 ${
            dragState.kind === 'dragging' && dragState.itemId !== task.id
              ? 'border-dashed border-blue-500'
              : ''
          }`}
          onPointerUp={() => handleDrop(task.id)}
        >
          <DraggableTaskCard task={task} />
        </div>
      ))}
    </div>
  )
}

5. Verificação de Evidências (Obrigatório)

Consulte react-kit/references/render-evidence-protocol.md para obter o protocolo completo de verificação. Os pontos principais:

  • Evidência R3 (Código Estático): A existência de código de animação ainda não prova que a animação funciona. Necessário evidência reproduzível.
  • Evidência R4 (Reproduzível): Capture capturas de tela (antes/depois) ou grave a animação.
  • Fallback: Se não for possível verificar, registre como [Não Verificado] com os quatro campos obrigatórios.

6. Padrões e Convenções

  • Nomes de arquivos: Use kebab-case para hooks e componentes de animação (ex.: use-drag.ts, view-transition.ts).
  • Estrutura de pastas: Código de animação em src/presentation/shared/ (hooks, lib, stores).
  • Store global: Use src/presentation/shared/stores/drag-store.ts apenas.
  • Acessibilidade: Sempre aplique prefers-reduced-motion.
  • Suporte a navegadores: Use fallbacks para navegadores não suportados (especialmente Firefox para View Transitions com types).

7. Perguntas Frequentes

P: Posso usar Framer Motion se o usuário insistir? R: Não. A política de zero bibliotecas é absoluta. Ofereça uma implementação alternativa com os Tiers descritos.

P: Quando usar <ViewTransition> do React 19.3? R: Apenas para atualizações de estado React marcadas como Transition (ex.: dentro de startTransition). Para outras atualizações DOM, use withViewTransition.

P: Como lidar com animações em scroll-triggered? R: Classifique como Tier 2 e use IntersectionObserver com CSS ou View Transitions.

P: E se eu precisar de gestos complexos? R: Implemente manualmente com Pointer Events + FSM (Tier 3). Consulte o exemplo useDrag.

P: Como garantir que a animação funciona em dispositivos móveis? R: Adicione touch-action: none (ou classe touch-none do Tailwind) e trate pointercancel.

P: O que fazer se o view-transition-name duplicar? R: Use IDs dinâmicos (ex.: task-${item.id}) para garantir unicidade.

P: Como desabilitar animações para usuários com prefers-reduced-motion? R: Use @media (prefers-reduced-motion: reduce) ou a variante motion-reduce:* do Tailwind em todos os Tiers.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Habilidades Relacionadas