Communitygithub.com

Motion Patterns

Motifs d'animation prêts pour la production pour React / Next.js — bouton, modale, toast, stagger, transitions de page, animations de sortie, défilement et mise en page — construits sur les tokens et ressorts de motion-foundations. À utiliser lors de l'animation d'un élément d'interface spécifique dans React ou Next.js.

Qu'est-ce que Motion Patterns ?

Ce skill fournit une bibliothèque complète de motifs d'animation prêts à l'emploi et adaptés à la production pour les applications React et Next.js. Il couvre les cas d'usage les plus courants : animations de boutons, modales, notifications toast, apparitions en liste avec effet de décalage (stagger), transitions de pages (notamment pour le App Router de Next.js), révélations au défilement et barres de progression, ainsi que des motifs de mise en page avancés comme les cartes extensibles, les accordéons et les transitions d'éléments partagés (shared-element). La force de ce skill repose sur son intégration stricte avec `motion-foundations` : tous les tokens de durée, d'easing, de distance et d'échelle proviennent de cette librairie, garantissant une cohérence visuelle parfaite dans tout le projet. Aucune valeur numérique ou durée n'est définie en dur dans les composants. Chaque motif respecte également des règles strictes de qualité et d'accessibilité : les rendus conditionnels sont systématiquement encapsulés dans `AnimatePresence` avec une clé, les animations de sortie ne sont jamais oubliées, et les modales incluent obligatoirement une gestion du focus, la fermeture par la touche Échap et les attributs ARIA appropriés. Les valeurs de stagger sont limitées à une plage de 0.05 à 0.10 seconde pour un rendu naturel. Le skill est conçu pour fonctionner avec `motion-foundations` et peut être complété par `motion-advanced` pour des interactions plus complexes (glisser-déposer, SVG, texte, etc.).

Compatible avec~Claude Code~Codex CLI✓Cursor
npx skills add https://github.com/KilianBStar/mes-skills-/tree/HEAD/.claude/skills/motion-patterns

Demander à votre IA préférée

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

Documentation

Que fait Motion Patterns ?

Motifs copier-coller pour les besoins d'animation UI les plus courants. Chaque motif ici est basé sur les tokens et ressorts de motion-foundations. Ne définissez pas de nouvelles durées ou courbes d'easing ici — importez-les.

Quand activer ce skill

  • Animer un bouton, une carte, une modale ou une notification toast
  • Construire des entrées de liste avec effet de décalage (stagger)
  • Mettre en place des transitions de page dans Next.js App Router
  • Ajouter des animations d'entrée ou de sortie à du contenu conditionnel
  • Implémenter des animations de révélation au défilement, une barre de progression liée au défilement ou des sections à effet sticky
  • Construire des cartes extensibles, des accordéons ou des transitions d'élément partagé

Résultats

Ce skill produit :

  • Des animations accessibles et compatibles SSR pour tous les composants d'interface standard
  • Des rendus conditionnels encapsulés dans AnimatePresence avec un comportement de sortie correct
  • Un composant d'encapsulation de transition de page pour Next.js App Router
  • Des motifs de révélation au défilement et de progression liée au défilement utilisant useScroll + useTransform
  • Des motifs d'animation de mise en page (layout, layoutId) pour les éléments extensibles et les fondus enchaînés

Principes

  • Chaque motif importe depuis motion-foundations. Pas de valeurs brutes.
  • Chaque rendu conditionnel est encapsulé dans AnimatePresence avec une key.
  • Les animations de sortie sont toujours définies en même temps que les animations d'entrée — jamais après coup.
  • layout est utilisé uniquement pour de petits déplacements isolés. Les grandes sous-arbres utilisent des transformations explicites.

Règles

  1. Encapsulez toujours les rendus conditionnels dans AnimatePresence avec une key sur l'enfant direct. Sans clé, les animations de sortie ne se déclenchent jamais.
  2. Définissez toujours exit lorsque vous définissez initial + animate. Une animation sans sortie est incomplète.
  3. Utilisez mode="wait" sur les transitions de page. L'entrée ne doit pas commencer avant la fin de la sortie.
  4. N'utilisez jamais layout sur des sous-arbres de plus de ~5 enfants ou profondément imbriqués dans le DOM. Utilisez des transformations explicites x/y à la place.
  5. L'intervalle de stagger doit rester entre 0.05s et 0.10s. En dessous, l'effet semble mécanique ; au-dessus, il semble lent.
  6. Les modales doivent toujours inclure : piège de focus, fermeture par touche Échap, verrouillage du défilement, role="dialog", aria-modal="true".
  7. Les révélations au défilement utilisent viewport={{ once: true }}. La répétition au défilement inverse est distrayante, pas informative.
  8. Toutes les valeurs des tokens sont importées depuis motion-foundations. Pas de nombres en ligne.

Guide de décision

Choisir le bon motif

SituationMotif
L'élément apparaît / disparaîtAnimatePresence
Liste d'éléments chargés en séquenceVariantes de stagger
Navigation entre routesEncapsulation de transition de page
L'élément change de taille sur placePropriété layout
Même élément se déplace entre contextes de pagelayoutId
L'élément apparaît lorsqu'il est visible dans la vuewhileInView
Valeur liée à la position de défilementuseScroll + useTransform

Quand utiliser mode="wait" vs mode="sync"

ModeUtilisation
waitTransitions de page, échanges de contenu (un à la fois)
syncNotifications empilées, éléments de liste (les chevauchements sont acceptables)
popLayoutÉléments retirés d'une liste à réorganisation

Concepts clés

Contrat AnimatePresence

Trois conditions doivent toujours être réunies :

  1. AnimatePresence encapsule la partie conditionnelle
  2. L'enfant direct a une key
  3. L'enfant a une propriété exit

Si l'une de ces conditions manque, l'animation de sortie échoue silencieusement.

layout vs layoutId

  • layout — anime le changement de taille/position de l'élément lui-même sur place
  • layoutId — relie deux éléments distincts, créant un fondu enchaîné entre eux lors des rendus

Utilisez layout="position" sur le texte à l'intérieur d'un conteneur extensible pour empêcher le réarrangement du texte d'être animé.

Exemples de code

Retour visuel d'un bouton

"use client"
import { motion } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

<motion.button
  whileHover={{ scale: motionTokens.scale.pop }}
  whileTap={{ scale: motionTokens.scale.press }}
  transition={springs.snappy}
/>

Liste à décalage (stagger)

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

const container = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.08,   // dans la règle 0.05–0.10
      delayChildren: 0.1,
    },
  },
}

const item = {
  hidden:  { opacity: 0, y: motionTokens.distance.md },
  visible: { opacity: 1, y: 0, transition: springs.gentle },
}

<motion.ul variants={container} initial="hidden" animate="visible">
  {items.map((i) => (
    <motion.li key={i.id} variants={item} />
  ))}
</motion.ul>

Modale

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

// Encapsuler au point d'appel :
// <AnimatePresence>{isOpen && <Modal key="modal" />}</AnimatePresence>

export function Modal({ onClose }: { onClose: () => void }) {
  return (
    <>
      {/* Fond */}
      <motion.div
        className="fixed inset-0 bg-black/50"
        initial={{ opacity: 0 }}
        animate={{ opacity: 1 }}
        exit={{ opacity: 0 }}
        onClick={onClose}
      />

      {/* Panneau — exigences d'accessibilité : piège de focus, fermeture par Échap,
          verrouillage du défilement, role="dialog", aria-modal="true" */}
      <motion.div
        role="dialog"
        aria-modal="true"
        className="fixed inset-x-4 top-1/2 -translate-y-1/2 rounded-xl bg-white p-6"
        initial={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        animate={{ opacity: 1, scale: 1, y: 0 }}
        exit={{
          opacity: 0,
          scale: motionTokens.scale.press,
          y: motionTokens.distance.sm,
        }}
        transition={springs.gentle}
      />
    </>
  )
}

Pile de notifications toast

"use client"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<AnimatePresence mode="sync">
  {toasts.map((t) => (
    <motion.div
      key={t.id}
      layout
      initial={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      animate={{ opacity: 1, x: 0, scale: 1 }}
      exit={{
        opacity: 0,
        x: motionTokens.distance.xl,
        scale: motionTokens.scale.subtle,
      }}
      transition={springs.snappy}
    />
  ))}
</AnimatePresence>

Transition de page (Next.js App Router)

// components/page-transition.tsx
"use client"
import { motion, AnimatePresence } from "motion/react"
import { usePathname } from "next/navigation"
import { motionTokens } from "@/lib/motion-tokens"

const variants = {
  initial: { opacity: 0, y: motionTokens.distance.sm },
  enter:   { opacity: 1, y: 0 },
  exit:    { opacity: 0, y: -motionTokens.distance.sm },
}

export function PageTransition({ children }: { children: React.ReactNode }) {
  const pathname = usePathname()
  return (
    <AnimatePresence mode="wait">
      <motion.div
        key={pathname}
        variants={variants}
        initial="initial"
        animate="enter"
        exit="exit"
        transition={{
          duration: motionTokens.duration.normal,
          ease: motionTokens.easing.smooth,
        }}
      >
        {children}
      </motion.div>
    </AnimatePresence>
  )
}

Révélation au défilement

"use client"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"

<motion.div
  initial={{ opacity: 0, y: motionTokens.distance.lg }}
  whileInView={{ opacity: 1, y: 0 }}
  viewport={{ once: true, margin: "-80px" }}   // once: true — règle 7
  transition={{ duration: motionTokens.duration.slow, ease: motionTokens.easing.smooth }}
/>

Barre de progression du défilement

"use client"
import { motion, useScroll } from "motion/react"

export function ScrollProgress() {
  const { scrollYProgress } = useScroll()
  return (
    <motion.div
      className="fixed top-0 left-0 h-1 bg-indigo-500 origin-left w-full"
      style={{ scaleX: scrollYProgress }}
    />
  )
}

Carte extensible

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { springs, motionTokens } from "@/lib/motion-tokens"

export function ExpandingCard({ title, body }: { title: string; body: string }) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div layout onClick={() => setExpanded(!expanded)} className="cursor-pointer">
      {/* layout="position" empêche le réarrangement du texte d'être animé */}
      <motion.h2 layout="position" className="font-semibold">
        {title}
      </motion.h2>

      <AnimatePresence>
        {expanded && (
          <motion.p
            key="body"
            initial={{ opacity: 0 }}
            animate={{ opacity: 1 }}
            exit={{ opacity: 0 }}
            transition={{ duration: motionTokens.duration.fast }}
          >
            {body}
          </motion.p>
        )}
      </AnimatePresence>
    </motion.div>
  )
}

Fondu enchaîné d'élément partagé

// Contexte source
<motion.img layoutId="hero-image" src={src} className="w-16 h-16 rounded" />

// Contexte de destination (même layoutId — motion gère la transition)
<motion.img layoutId="hero-image" src={src} className="w-full rounded-xl" />

Accordéon

<motion.div
  initial={false}
  animate={{ opacity: open ? 1 : 0, scaleY: open ? 1 : 0 }}
  style={{ transformOrigin: "top", overflow: "hidden" }}
  transition={{
    duration: motionTokens.duration.normal,
    ease: motionTokens.easing.smooth,
  }}
> {children}
</motion.div>

Exemple complet

Une liste à décalage qui apparaît au montage, gère la présence conditionnelle et respecte la réduction de mouvement — combinant tokens, ressorts, AnimatePresence et le hook d'accessibilité de motion-foundations :

"use client"
import { useState } from "react"
import { motion, AnimatePresence } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"

const containerVariants = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.08, delayChildren: 0.1 },
  },
}

function ListItem({ label, onRemove }: { label: string; onRemove: () => void }) {
  const safe = useSafeMotion(motionTokens.distance.sm)
  return (
    <motion.li
      variants={{
        hidden:  safe.initial,
        visible: safe.animate,
      }}
      exit={safe.exit}
      transition={springs.gentle}
      className="flex items-center justify-between p-3 rounded-lg bg-white shadow-sm"
    >
      <span>{label}</span>
      <button onClick={onRemove}>Supprimer</button>
    </motion.li>
  )
}

export function AnimatedList({ items, onRemove }: {
  items: { id: string; label: string }[]
  onRemove: (id: string) => void
}) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      animate="visible"
      className="space-y-2"
    >
      <AnimatePresence mode="popLayout">
        {items.map((item) => (
          <ListItem
            key={item.id}
            label={item.label}
            onRemove={() => onRemove(item.id)}
          />
        ))}
      </AnimatePresence>
    </motion.ul>
  )
}

Contraintes / Non-Objectifs

Ce skill ne couvre pas :

  • Les définitions de tokens et de ressorts → voir motion-foundations
  • Les interactions de glisser, les gestes de balayage, les listes réorganisables → voir motion-advanced
  • Les animations de texte (révélation de mots/caractères, compteurs) → voir motion-advanced
  • Le dessin ou morphing de chemins SVG → voir motion-advanced
  • Les hooks d'animation personnalisés → voir motion-advanced
  • Les transitions CSS uniquement sans utiliser motion/react

Anti-patrons

Anti-patronRègle violéeCorrection
Enfant d'AnimatePresence sans keyRègle 1Ajouter une key stable à l'enfant direct
initial + animate sans exitRègle 2Toujours définir les trois ensemble
Transition de page sans mode="wait"Règle 3Ajouter mode="wait" à AnimatePresence
layout sur une liste de 50 élémentsRègle 4Utiliser mode="popLayout" ou des transformations explicites
staggerChildren: 0.2 sur une liste de 10 élémentsRègle 5Limiter à 0.08–0.10
Modale sans piège de focusRègle 6Ajouter focus-trap-react ou Radix Dialog
whileInView sans viewport={{ once: true }}Règle 7Les entrées répétées distraient, ne renseignent pas
transition={{ duration: 0.3 }} en ligneRègle 8Utiliser motionTokens.duration.normal

Compétences associées

  • motion-foundations — définit tous les tokens, ressorts, le hook useSafeMotion et les gardes SSR que chaque motif importe ici. Doit être configuré en premier.
  • motion-advanced — étend ces motifs avec le glisser, les gestes, SVG, le texte, les hooks personnalisés et le séquencement impératif. Ne redéfinit aucun motif de ce skill.

Skills associés