Communitygithub.com

joo6077/react-animation

Habilidad para implementar animaciones en React sin librerías externas, usando Tailwind CSS, View Transitions API y Pointer Events con máquina de estados.

¿Qué es react-animation?

Esta habilidad proporciona un enfoque estructurado para implementar animaciones en proyectos React sin depender de librerías externas como framer-motion o react-spring. Se basa en un sistema de tres niveles (Tiers): Tier 1 para animaciones simples con Tailwind CSS y keyframes nativos, Tier 2 para transiciones de vista y elementos compartidos usando la View Transitions API, y Tier 3 para interacciones complejas como drag & drop con Pointer Events y una máquina de estados finitos (FSM). La habilidad es compatible con Claude Code, Cursor, Codex y otros agentes que sigan el protocolo react-kit. Incluye reglas estrictas como la prohibición de bibliotecas de animación (Gotcha #1), la verificación de compatibilidad cross-browser (especialmente Firefox y Chrome), y la exigencia de evidencia de animación funcional mediante el protocolo de evidencia de renderizado. También cubre aspectos avanzados como animaciones impulsadas por scroll, `view-transition-name: match-element`, y la integración con `<ViewTransition>` de React 19.3 como API estable.

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

Preguntar en tu IA favorita

Abre un nuevo chat con esta habilidad de agente ya precargada.

Documentación

React Animation

El plugin instalado no tiene docs/react/ — si no se puede abrir la ruta docs/... de este archivo, se debe leer agregando la misma ruta después de https://raw.githubusercontent.com/joo6077/claude-plugins/main/. Si aún así no se puede leer, se debe indicar que no se pudo leer sin inventar el contenido.

Advertencias

  1. Principio de cero bibliotecas — sin excepciones: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css están prohibidos de instalar e importar. Si estas importaciones existen en el código base, /react-audit emitirá un fallo de compilación. Incluso si el usuario lo solicita, se debe sugerir una implementación alternativa y rechazar el uso de la biblioteca.

  2. Probar desde el nivel más bajo: Al recibir una solicitud de "animación", es común cometer el error de usar directamente el Tier 3. Seguir las reglas de decisión: verificar primero si se puede resolver con Tier 1, y solo subir a un Tier superior si no es posible.

  3. Verificar el plugin tailwindcss-animate: Las utilidades extendidas como animate-in, fade-in-*, slide-in-from-* requieren tailwindcss-animate. En proyectos con shadcn instalado, suele estar incluido por defecto, pero si no, se debe guiar al usuario para ejecutar pnpm add -D tailwindcss-animate y registrar el plugin en tailwind.config.ts.

  4. Unicidad de view-transition-name: Si dos elementos tienen el mismo nombre simultáneamente, se produce el error "duplicate name". Se debe usar un id dinámico (view-transition-name: task-${item.id}) para garantizar la unicidad.

  5. Manejo obligatorio de Pointer cancel: Si no se maneja el evento pointercancel, el estado de arrastre puede filtrarse al cambiar de pestaña, enfocar otra aplicación o desmontar el elemento. En el manejador onPointerCancel, se debe reiniciar la FSM a idle.

  6. Si falta touch-action: none, el arrastre móvil no funciona: En elementos de arrastre basados en eventos pointer, se debe agregar la clase Tailwind touch-none para bloquear el desplazamiento/zoom predeterminado del navegador.

  7. El store de arrastre de Zustand es una única instancia global: No crear stores de arrastre separados por funcionalidad. Todos deben compartir el archivo src/presentation/shared/stores/drag-store.ts.

  8. Falta de prefers-reduced-motion: En todos los Tiers, se debe deshabilitar la animación con @media (prefers-reduced-motion: reduce) o la variante Tailwind motion-reduce:*. Si falta, /react-audit emitirá una advertencia.

  9. La animación es solo para presentación: No importar eventos pointer, view transitions o el store de arrastre de Zustand en las capas domain/ o data/. El código relacionado con animaciones debe estar únicamente en src/presentation/.

  10. Firefox no soporta el parámetro types: La opción types: [...] en document.startViewTransition({ update, types: [...] }) solo es compatible de forma estable en navegadores Chromium a partir de 2026. En el wrapper withViewTransition, manejar el fallback con try/catch.

  11. Compatibilidad de navegadores para animaciones impulsadas por scroll (actualizado el 13/08/2026): animation-timeline: scroll() / scroll-timeline tiene una cobertura global del 85.43% según Can I Use — Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+. La afirmación de que Firefox lo tiene detrás de un flag está desactualizada. Sin embargo, según MDN, view() aún no es Baseline, por lo que se mantiene la política de envolver con @supports (animation-timeline: scroll()) / @supports (animation-timeline: view()) y usar estilos estáticos como fallback en caso de no soporte.

  12. Uso de view-transition-name: match-element (Chrome 137+): En lugar de asignar nombres únicos manualmente a decenas de elementos, se puede usar el valor CSS view-transition-name: match-element para que el navegador empareje los elementos automáticamente. Sin embargo, a partir del segundo trimestre de 2026, esto es exclusivo de Chromium, por lo que se debe mantener una ruta de fallback.

  13. El movimiento no se demuestra con código estático — se requiere evidencia antes de declarar finalización (E2): El producto de esta habilidad solo se puede verificar mediante la reproducción. El hecho de que exista una clase animate-* o que el código llame a startViewTransition es evidencia R3 (estática), no evidencia de que la animación se haya reproducido realmente — keyframes no definidos, prefers-reduced-motion siempre activo, clipping por overflow: hidden del padre, o la entrada al fallback de un navegador no compatible, todo termina silenciosamente en "no pasa nada". Justo antes de finalizar, completar la lista de verificación §4 de react-kit/references/render-evidence-protocol.md. Al verificar con comparación de capturas, señalar dos momentos: antes y después — una sola imagen fija produce el mismo resultado para cualquier movimiento, por lo que no es un oráculo (§3 d). Si no se puede obtener evidencia, agregar [No verificado] y llenar cuatro campos (lo que bloquea · las soluciones intentadas · la razón fuera de control · el comando para re-verificar — según el protocolo §2) para reportar como finalización parcial.

    No introducir bibliotecas de animación para obtener evidencia. La política de bibliotecas (Advertencia #1) tiene prioridad sobre este protocolo y no se relaja por ninguna conveniencia de verificación.

  14. La captura de referencia se toma antes de editar — Ejecutar el Paso 0 de §1 y el punto 1 del orden de comparación de §2 de react-kit/references/render-evidence-protocol.md antes de la primera edición, y dejar en la respuesta el resultado (repetición · tabla de convenciones · ruta de captura del estado actual y la marca de lo que debe cambiar). Después de editar, ya no se puede capturar el estado anterior a la edición.

  15. El Tier 2 se segmenta por tipo de actualización — <ViewTransition> de React 19.3 es una API estable (añadido el 26/09/2026): <ViewTransition> importado desde react se convierte en una API estable, no experimental, a partir de la versión 19.3. Se ejecuta cuando los hijos envueltos cambian de estilo, se montan o desmontan mediante actualizaciones marcadas como Transiciones (actualizaciones dentro de startTransition, Suspense reveal, useDeferredValue), y solo funciona en el DOM (fuente: 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.»). Por lo tanto, para las actualizaciones de estado de React marcadas como Transiciones, usar <ViewTransition>, y para otras actualizaciones del DOM (actualizaciones síncronas no marcadas como Transiciones, manipulación del DOM fuera de React), usar el wrapper withViewTransition de §3.1. No eliminar el wrapper. El componente dentro del paquete react no infringe el principio de cero bibliotecas (#1). Si la versión de react del proyecto es inferior a 19.3, usar solo el wrapper.

Proceso

1. Decisión automática de nivel

Si no hay flag --tier, analizar la solicitud del usuario y seleccionar el nivel más bajo.

Palabra clave / EscenarioNivel asignado
"fade in", "slide up", "scale", "pulse", "bounce", "hover effect", "opacity", "shimmer", "animación de entrada"T1
"transición de skeleton → carga completa", "hover de botón", "accordion", "open/close de modal", "cambio de estado"T1
"animación de scroll", "scroll-driven", "parallax", "barra de progreso de scroll", "basado en scroll"T1
"transición de página", "shared element", "grid to board", "transición de vista", "transición de ruta", "cambio de estructura DOM"T2
"scroll-triggered", "activar al cruzar el scroll", "iniciar animación en una posición específica"T2
"drag & drop", "lista ordenable", "sortable", "kanban", "arrastre", "soltar", "gesture", "línea de conexión con flecha"T3

Si el límite entre T2/T3 es ambiguo, consultar al agente animation-architect-react.

Resumen de 3 niveles:

NivelHerramientaEscenario de aplicaciónDificultad
T1animate-* de Tailwind + @keyframes CSS + scroll-drivenCambio de estado, hover, bucle simple, vinculado a scrollBaja
T2API View Transitions (wrapper withViewTransition) · <ViewTransition> de React 19.3 (solo actualizaciones marcadas como Transición — Advertencia 15)Transición de vista/ruta, shared element, FLIPMedia
T3Pointer Events + FSM + requestAnimationFrameDrag & drop, gestos, líneas de conexión SVGAlta

2. Nivel 1 — Implementación con Tailwind + CSS

2.1 Utilidades integradas de Tailwind

CategoríaUtilidadEjemplo
Transicióntransition-transform, transition-colors, transition-opacityhover:scale-105 transition-transform duration-200
Duraciónduration-75~duration-1000duration-300
Easingease-linear, ease-in, ease-out, ease-in-outease-in-out
Animación (integrada)animate-spin, animate-ping, animate-pulse, animate-bounceCarga, notificación
Retardodelay-75~delay-1000Efecto stagger

2.2 Utilidades extendidas de tailwindcss-animate

Animaciones de entrada/salida proporcionadas por el 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-*

Combinar con el selector de atributo data-[state=...] para implementar animaciones basadas en estado:

// Ejemplo de abrir/cerrar un drawer
<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
  "
>
  Contenido del drawer
</div>

2.3 Declaración directa de CSS @keyframes

Las animaciones personalizadas que no cubren las utilidades de Tailwind se declaran en el bloque @theme de 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 Ejemplos de escenarios del Nivel 1

Entrada con fade-in:

<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
  Entrada con fade al cargar la página
</div>

Botón con hover scale:

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

Entrada de lista con 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 Animaciones impulsadas por scroll (CSS nativo)

Implementar animaciones vinculadas a la posición de scroll sin JavaScript usando CSS animation-timeline. A fecha del 13/08/2026, scroll() / scroll-timeline es compatible en Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ (cobertura global del 85.43% según Can I Use), mientras que view() aún no es Baseline según MDN. En ambos casos, envolver con @supports para un fallback elegante en navegadores no compatibles.

Barra de progreso 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 al entrar un elemento (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">
  Contenido que aparece al hacer scroll
</section>

Nota: Las animaciones scroll-triggered (Chrome 145, 2026) son un concepto diferente que activa una animación basada en tiempo al cruzar un desplazamiento de scroll específico. No confundir con scroll-driven. Las scroll-triggered se clasifican como candidatas a Nivel 2.

3. Nivel 2 — Implementación de View Transitions API

Las View Transitions same-document son Baseline 2025 según MDN, con una cobertura global del 90.2% según Can I Use — Chrome/Edge 111+, Safari 18+, Firefox 144+ (actualizado el 13/08/2026). Proporcionan animación FLIP automática al cambiar el DOM dentro de una SPA.

Por otro lado, las transiciones cross-document / MPA basadas en @view-transition y algunas funciones detalladas aún tienen disponibilidad limitada. Al usarlas para transiciones de ruta, se debe establecer la ruta same-document como predeterminada y mantener la detección de características + fallback (§6, brecha de cobertura estándar n.º 8).

3.1 Creación del wrapper withViewTransition

Crear el archivo si no existe:

// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
  updateDOM: () => void,
  options?: { types?: string[] },
): void {
  // Respetar prefers-reduced-motion
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    updateDOM()
    return
  }
  // Feature detection (fallback para navegadores antiguos)
  if (typeof document.startViewTransition !== 'function') {
    updateDOM()
    return
  }
  // Comprobar soporte del parámetro types (Firefox no lo soporta en implementaciones iniciales)
  try {
    if (options?.types && options.types.length > 0) {
      document.startViewTransition({ update: updateDOM, types: options.types })
    } else {
      document.startViewTransition(updateDOM)
    }
  } catch {
    updateDOM()
  }
}

3.2 Elemento compartido con view-transition-name CSS

Identificar elementos como "el mismo" aunque cambie la estructura del DOM para una transición suave:

// Garantizar unicidad con id dinámico (prohibido nombres duplicados)
<div
  className="rounded-lg border p-4"
  style={{ viewTransitionName: `task-${task.id}` }}
>
  {task.title}
</div>

3.3 Transición de vista grid ↔ board (escenario representativo)

// 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' ? 'A vista board' : 'A vista 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 Ajuste fino de la transición CSS

/* globals.css — duración + easing de la transición */
::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;
}

/* Transición inmediata con reduced-motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

4. Nivel 3 — Implementación de primitivas personalizadas con Pointer

Implementar directamente la API Pointer Events + máquina de estados finitos (FSM) sin bibliotecas.

4.1 Creación del hook useDrag

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

// FSM de 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(dragReducer, { kind: 'idle' })
  const ref = useRef<HTMLElement | null>(null)

  const onPointerDown = useCallback((e: React.PointerEvent) => {
    if (state.kind !== 'idle') return
    // Capturar el puntero para seguimiento fiable
    // (Se puede usar e.currentTarget.setPointerCapture(e.pointerId))
    dispatch({ type: 'start', x: e.clientX, y: e.clientY, pointerId: e.pointerId })
  }, [state.kind])

  const onPointerMove = useCallback((e: React.PointerEvent) => {
    dispatch({ type: 'move', x: e.clientX, y: e.clientY })
  }, [])

  const onPointerUp = useCallback((e: React.PointerEvent) => {
    dispatch({ type: 'drop' })
  }, [])

  const onPointerCancel = useCallback((e: React.PointerEvent) => {
    // Restablecer FSM a idle (Advertencia #5)
    dispatch({ type: 'cancel' })
  }, [])

  const resolve = useCallback(() => {
    dispatch({ type: 'resolved' })
  }, [])

  return {
    state,
    handlers: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel },
    ref,
    resolve,
  }
}

Individual skills in this repo

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

Skills relacionados