Communitygithub.com

joo6077/react-animation

Règle d'animation React sans bibliothèque : pas d'import de motion/framer-motion, implémentation via Tailwind + View Transitions API + Pointer Events FSM.

Qu'est-ce que react-animation ?

Ce skill définit une politique stricte pour les animations React sans aucune bibliothèque tierce (Motion, react-spring, etc.). Il impose un système à 3 niveaux (Tier) : Tier 1 utilise Tailwind CSS et les animations CSS natives (scroll-driven), Tier 2 utilise l'API View Transitions (avec un wrapper `withViewTransition` et le composant stable `<ViewTransition>` de React 19.3), et Tier 3 implémente le glisser-déposer via Pointer Events + Machine à États Finis (FSM). Le skill gère la détection de fonctionnalités (ex. `view-transition-name: match-element` pour Chrome 137+), les fallbacks pour `prefers-reduced-motion`, la gestion du pointeur (toucher, annulation) et les détails de compatibilité navigateur (Firefox, types, etc.). Il met l'accent sur la preuve d'exécution (non statique) et exige un protocole de capture avant/après (Render Evidence Protocol).

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

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

React Animation

Le plugin installé n'a pas de dossier docs/react/ — si le chemin docs/... de ce fichier ne peut pas être ouvert, ajoutez le même chemin après https://raw.githubusercontent.com/joo6077/claude-plugins/main/ et essayez de le lire. Si la lecture échoue encore, ne fabriquez pas de contenu et indiquez que la lecture a échoué.

Pièges

  1. Principe zéro bibliothèque — aucune exception: Motion (framer-motion) / dnd-kit / react-spring / react-transition-group / @formkit/auto-animate / react-dnd / gsap / lottie-react / react-beautiful-dnd / animate.css sont interdits d'installation et d'import. Si une instruction import de ces bibliothèques existe dans la base de code, /react-audit émettra une erreur de build. Même si l'utilisateur le demande, proposez une implémentation alternative et refusez l'utilisation de la bibliothèque.

  2. Essayez d'abord le niveau le plus bas: Lors d'une demande d'"animation", il est fréquent de commencer par le Tier 3. Suivez les règles de décision : vérifiez d'abord si le Tier 1 est possible, et ne montez à un niveau supérieur que si ce n'est pas possible.

  3. Vérifiez le plugin tailwindcss-animate: Les utilitaires étendus comme animate-in, fade-in-*, slide-in-from-* nécessitent tailwindcss-animate. Dans les projets shadcn, il est inclus par défaut, mais sinon, guidez l'utilisateur pour exécuter pnpm add -D tailwindcss-animate et enregistrer le plugin dans tailwind.config.ts.

  4. Unicité de view-transition-name: Si deux éléments ont le même nom simultanément, une erreur "duplicate name" se produit. Utilisez impérativement un identifiant dynamique (view-transition-name: task-${item.id}) pour garantir l'unicité.

  5. Gestion obligatoire de l'annulation du pointeur: Ne pas gérer l'événement pointercancel entraîne une fuite de l'état de glissement lors du changement d'onglet, de la mise en avant d'une autre application ou du démontage de l'élément. Dans le gestionnaire onPointerCancel, réinitialisez impérativement la FSM à idle.

  6. Sans touch-action: none, pas de glissement mobile: Ajoutez impérativement la classe Tailwind touch-none aux éléments de glissement basés sur les événements pointeur pour bloquer le défilement/zoom par défaut du navigateur.

  7. Le store Zustand de glissement est une instance globale unique: Ne créez pas de store de glissement séparé par fonctionnalité. Toutes les fonctionnalités partagent le même fichier src/presentation/shared/stores/drag-store.ts.

  8. Absence de prefers-reduced-motion: À tous les niveaux, désactivez les animations avec @media (prefers-reduced-motion: reduce) ou la variante Tailwind motion-reduce:*. /react-audit émettra un avertissement en cas d'absence.

  9. Les animations sont réservées à la présentation: N'importez pas les événements pointeur / view transition / store Zustand de glissement dans les couches domain/ ou data/. Le code lié aux animations se trouve uniquement dans src/presentation/.

  10. Firefox ne supporte pas le paramètre types: L'option types de document.startViewTransition({ update, types: [...] }) n'est supportée de manière stable que sur les navigateurs Chromium (en 2026). Gérez le fallback avec try/catch dans le wrapper withViewTransition.

  11. Support navigateur des animations scroll-driven (mis à jour le 2026-08-13): animation-timeline: scroll() / scroll-timeline a une couverture globale de 85,43% (Can I Use) — Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+. L'affirmation selon laquelle Firefox est derrière un flag est obsolète. Cependant, selon MDN, view() n'est pas encore Baseline, donc encadrez avec @supports (animation-timeline: scroll()) / @supports (animation-timeline: view()) et conservez la politique de fallback avec des styles statiques en cas de non-support.

  12. Utilisation de view-transition-name: match-element (Chrome 137+): Au lieu d'attribuer manuellement des noms uniques à des dizaines d'éléments, utilisez la valeur CSS view-transition-name: match-element pour que le navigateur fasse correspondre les éléments automatiquement. Cependant, en T2 2026, cela est réservé à Chromium, donc conservez un chemin de fallback.

  13. Le mouvement n'est pas prouvé par du code statique — preuve indispensable avant la déclaration de fin (E2): Les résultats de cette compétence ne peuvent être vérifiés que s'ils sont joués. Le fait qu'une classe animate-* soit présente, ou que le code appelle startViewTransition est une preuve R3 (statique) et ne prouve pas que l'animation a réellement été jouée — des keyframes non définies, prefers-reduced-motion toujours actif, le clipping par overflow: hidden du parent, ou l'entrée dans un fallback de navigateur non supporté se terminent tous silencieusement par "rien ne se passe". Juste avant la fin, exécutez la checklist §4 de react-kit/references/render-evidence-protocol.md. Lors de la vérification par comparaison d'instantanés, désignez deux instants (avant/après) — une seule image fixe donne le même résultat quelle que soit l'animation, donc ce n'est pas un oracle (§3 d). Si vous ne parvenez pas à obtenir de preuve, marquez [Non vérifié], remplissez les quatre cases (ce qui bloque · contournement tenté · raison du contrôle impossible · commande de re-vérification — protocole §2) et signalez comme une fin partielle.

    N'introduisez pas de bibliothèque d'animation pour obtenir des preuves. La politique des bibliothèques (Piège #1) est supérieure à ce protocole et n'est assouplie par aucune commodité de vérification.

  14. La capture de référence s'effectue avant l'édition — Exécutez l'étape 0 de la §1 et le point 1 de l'ordre de comparaison répétée de la §2 de react-kit/references/render-evidence-protocol.md avant la première édition, et laissez le résultat (résumé, tableau des conventions, chemin de capture de l'état actuel et marqueurs à modifier) dans la réponse. Après l'édition, il est impossible de reprendre l'état antérieur.

  15. Le Tier 2 est divisé par type de mise à jour — <ViewTransition> de React 19.3 est une API stable (ajouté le 2026-09-26): <ViewTransition> importé depuis react est devenu une API stable en 19.3 (et non une API expérimentale). Il s'exécute lorsque les enfants enveloppés changent de style ou sont montés/démontés par des mises à jour marquées comme Transition (mises à jour dans startTransition, Suspense reveal, useDeferredValue), et fonctionne uniquement sur le DOM (source : 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. »). Donc, pour les mises à jour d'état React marquées comme Transition, utilisez <ViewTransition> ; pour les autres mises à jour DOM (mises à jour synchrones non Transition, manipulations DOM hors React), continuez à utiliser le wrapper withViewTransition de la §3.1. Ne supprimez pas le wrapper. Comme c'est un composant du package react, il n'enfreint pas la règle zéro bibliothèque (#1). Si la version react du projet est inférieure à 19.3, utilisez uniquement le wrapper.

Processus

1. Décision automatique du niveau

Si le drapeau --tier n'est pas présent, analysez la demande de l'utilisateur et sélectionnez le niveau le plus bas.

Mot-clé / ScénarioNiveau attribué
"fade in", "slide up", "scale", "pulse", "bounce", "effet hover", "opacity", "shimmer", "animation d'entrée"T1
"transition squelette chargement → terminé", "bouton hover", "accordéon", "modal open/close", "changement d'état"T1
"animation de défilement", "scroll-driven", "parallax", "barre de progression de défilement", "basé sur le défilement"T1
"transition de page", "élément partagé", "grid to board", "transition de vue", "transition de route", "changement de structure DOM"T2
"déclenché par le défilement", "déclenchement à l'intersection du défilement", "démarrage d'animation à une position spécifique"T2
"glisser-déposer", "liste triable", "sortable", "kanban", "glisser", "déposer", "geste", "ligne de connexion par flèche"T3

Si la frontière T2/T3 est ambiguë, demandez conseil à l'agent animation-architect-react.

Résumé des 3 niveaux :

NiveauOutilScénario d'applicationDifficulté
T1Tailwind animate-* + CSS @keyframes + scroll-drivenChangement d'état, hover, boucle simple, défilement liéFaible
T2API View Transitions (wrapper withViewTransition) · React 19.3 <ViewTransition> (uniquement pour les mises à jour marquées Transition — Piège 15)Transition de vue/route, élément partagé, FLIPMoyenne
T3Pointer Events + FSM + requestAnimationFrameGlisser-déposer, gestes, lignes de connexion SVGÉlevée

2. Tier 1 — Implémentation Tailwind + CSS

2.1 Utilitaires intégrés Tailwind

CatégorieUtilitaireExemple
Transitiontransition-transform, transition-colors, transition-opacityhover:scale-105 transition-transform duration-200
Duréeduration-75~duration-1000duration-300
Accélérationease-linear, ease-in, ease-out, ease-in-outease-in-out
Animation (intégrée)animate-spin, animate-ping, animate-pulse, animate-bounceChargement, notification
Délaidelay-75~delay-1000Effet stagger

2.2 Utilitaires étendus de tailwindcss-animate

Animations d'entrée/sortie fournies par le 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-*

Combinez avec les sélecteurs d'attribut data-[state=...] pour implémenter des animations basées sur l'état :

// Exemple d'ouverture/fermeture de tiroir
<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
  "
>
  Contenu du tiroir
</div>

2.3 Déclaration directe de @keyframes CSS

Pour les animations personnalisées non couvertes par les utilitaires Tailwind, déclarez-les dans le bloc @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;
  }
}

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

2.4 Exemples de scénarios Tier 1

Entrée fade-in :

<div className="animate-in fade-in-0 duration-500 motion-reduce:animate-none">
  Entrée en fondu au chargement de la page
</div>

Bouton avec effet hover scale :

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

Entrée de liste avec 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 Animations scroll-driven (CSS natif)

Implémentez des animations liées à la position de défilement sans JavaScript, avec CSS animation-timeline. Au 2026-08-13, scroll() / scroll-timeline est supporté sur Chrome/Edge 115+, Safari/iOS 26+, Firefox 156+ (couverture globale 85,43% selon Can I Use), et view() n'est pas encore Baseline selon MDN. Dans les deux cas, encadrez avec @supports pour un fallback gracieux sur les navigateurs non supportés.

Barre de progression de défilement :

/* 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 à l'entrée d'un élément (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">
  Contenu apparaissant lors du défilement
</section>

Attention : Les animations scroll-triggered (Chrome 145, 2026) sont un concept distinct qui déclenche une animation basée sur le temps au croisement d'un décalage de défilement spécifique. Ne les confondez pas avec les animations scroll-driven. Les animations scroll-triggered sont classées comme candidates au Tier 2.

3. Tier 2 — Implémentation de l'API View Transitions

Les View Transitions same-document sont Baseline 2025 selon MDN, couverture globale 90,2% selon Can I Use — Chrome/Edge 111+, Safari 18+, Firefox 144+ (mis à jour le 2026-08-13). Animation FLIP automatique lors des changements DOM dans une SPA.

En revanche, les transitions cross-document / MPA basées sur @view-transition et certaines fonctionnalités avancées sont encore limited availability. Pour les transitions de route, utilisez le chemin same-document par défaut et conservez la détection de fonctionnalité + fallback (§6, espace de couverture standard n°8).

3.1 Création du wrapper withViewTransition

Créez le fichier s'il n'existe pas :

// src/presentation/shared/lib/view-transition.ts
export function withViewTransition(
  updateDOM: () => void,
  options?: { types?: string[] },
): void {
  // Respect de prefers-reduced-motion
  if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
    updateDOM()
    return
  }
  // Détection de fonctionnalité (fallback pour navigateurs anciens)
  if (typeof document.startViewTransition !== 'function') {
    updateDOM()
    return
  }
  // Gestion du support du paramètre types (non supporté par l'implémentation initiale de Firefox)
  try {
    if (options?.types && options.types.length > 0) {
      document.startViewTransition({ update: updateDOM, types: options.types })
    } else {
      document.startViewTransition(updateDOM)
    }
  } catch {
    updateDOM()
  }
}

3.2 Élément partagé CSS view-transition-name

Même si la structure DOM change, identifiez-le comme "même élément" pour une transition fluide :

// Garantie d'unicité avec un identifiant dynamique (pas de nom en double)
<div
  className="rounded-lg border p-4"
  style={{ viewTransitionName: `task-${task.id}` }}
>
  {task.title}
</div>

3.3 Transition de vue grille ↔ board (scénario représentatif)

// 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' ? 'Vers vue board' : 'Vers vue grille'}
    </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 Réglages fins de la transition CSS

/* globals.css — durée + accélération de la transition */
::view-transition-group(*) {
  animation-duration: 0.4s;
  animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
}

/* Ajustement individuel d'un élément spécifique */
::view-transition-group(hero-image) {
  animation-duration: 0.6s;
}

/* Transition immédiate en mode reduced-motion */
@media (prefers-reduced-motion: reduce) {
  ::view-transition-group(*) {
    animation-duration: 0.01ms;
  }
}

4. Tier 3 — Implémentation de primitives Pointer personnalisées

Implémentez directement sans bibliothèque, avec l'API Pointer Events + Machine à États Finis (FSM).

4.1 Création du hook useDrag

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

// FSM 3 états : 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
…

Individual skills in this repo

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

Skills associés