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
-
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. -
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.
-
Vérifiez le plugin
tailwindcss-animate: Les utilitaires étendus commeanimate-in,fade-in-*,slide-in-from-*nécessitenttailwindcss-animate. Dans les projets shadcn, il est inclus par défaut, mais sinon, guidez l'utilisateur pour exécuterpnpm add -D tailwindcss-animateet enregistrer le plugin danstailwind.config.ts. -
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é. -
Gestion obligatoire de l'annulation du pointeur: Ne pas gérer l'événement
pointercancelentraî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 gestionnaireonPointerCancel, réinitialisez impérativement la FSM àidle. -
Sans
touch-action: none, pas de glissement mobile: Ajoutez impérativement la classe Tailwindtouch-noneaux éléments de glissement basés sur les événements pointeur pour bloquer le défilement/zoom par défaut du navigateur. -
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. -
Absence de
prefers-reduced-motion: À tous les niveaux, désactivez les animations avec@media (prefers-reduced-motion: reduce)ou la variante Tailwindmotion-reduce:*./react-auditémettra un avertissement en cas d'absence. -
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/oudata/. Le code lié aux animations se trouve uniquement danssrc/presentation/. -
Firefox ne supporte pas le paramètre
types: L'optiontypesdedocument.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 wrapperwithViewTransition. -
Support navigateur des animations scroll-driven (mis à jour le 2026-08-13):
animation-timeline: scroll()/scroll-timelinea 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. -
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 CSSview-transition-name: match-elementpour que le navigateur fasse correspondre les éléments automatiquement. Cependant, en T2 2026, cela est réservé à Chromium, donc conservez un chemin de fallback. -
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 appellestartViewTransitionest une preuve R3 (statique) et ne prouve pas que l'animation a réellement été jouée — des keyframes non définies,prefers-reduced-motiontoujours actif, le clipping paroverflow: hiddendu 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 dereact-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.
-
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.mdavant 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. -
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é depuisreactest 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 dansstartTransition, 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 wrapperwithViewTransitionde la §3.1. Ne supprimez pas le wrapper. Comme c'est un composant du packagereact, il n'enfreint pas la règle zéro bibliothèque (#1). Si la versionreactdu 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énario | Niveau 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 :
| Niveau | Outil | Scénario d'application | Difficulté |
|---|---|---|---|
| T1 | Tailwind animate-* + CSS @keyframes + scroll-driven | Changement d'état, hover, boucle simple, défilement lié | Faible |
| T2 | API 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é, FLIP | Moyenne |
| T3 | Pointer Events + FSM + requestAnimationFrame | Glisser-déposer, gestes, lignes de connexion SVG | Élevée |
2. Tier 1 — Implémentation Tailwind + CSS
2.1 Utilitaires intégrés Tailwind
| Catégorie | Utilitaire | Exemple |
|---|---|---|
| Transition | transition-transform, transition-colors, transition-opacity | hover:scale-105 transition-transform duration-200 |
| Durée | duration-75~duration-1000 | duration-300 |
| Accélération | ease-linear, ease-in, ease-out, ease-in-out | ease-in-out |
| Animation (intégrée) | animate-spin, animate-ping, animate-pulse, animate-bounce | Chargement, notification |
| Délai | delay-75~delay-1000 | Effet stagger |
2.2 Utilitaires étendus de tailwindcss-animate
Animations d'entrée/sortie fournies par le plugin tailwindcss-animate :
animate-in/animate-outfade-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
…