Motion pour React : Créer des animations, gestes et transitions de qualité production

Introduction
Les animations transforment une bonne interface en une interface exceptionnelle. Elles guident les utilisateurs à travers les changements d'état, fournissent un retour sur les interactions et donnent vie aux applications. Motion (anciennement Framer Motion) est la bibliothèque d'animation la plus populaire pour React, utilisée par des entreprises comme Vercel, Linear et Stripe pour créer des interfaces fluides et performantes.
Dans ce tutoriel, vous apprendrez Motion depuis les bases. En commençant par de simples fondus, vous progresserez à travers les gestes, les animations de mise en page, les effets de défilement et les séquences orchestrées complexes. À la fin, vous aurez construit une galerie de fiches produits entièrement animée avec des effets de survol interactifs, des transitions de mise en page fluides et des révélations au défilement.
Ce que vous apprendrez
- Installer et configurer Motion dans un projet Next.js ou React
- Animer des composants avec le composant
motionet la propanimate - Créer des gestes interactifs : survol, clic, glissement et focus
- Construire des animations de mise en page qui transitionnent en douceur entre les états
- Utiliser
AnimatePresencepour les animations d'entrée et de sortie - Implémenter des animations pilotées par le défilement avec
useScrolletuseTransform - Orchestrer des séquences complexes avec des variants et des effets de décalage
- Optimiser les performances des animations pour la production
Prérequis
Avant de commencer, assurez-vous d'avoir :
- Node.js 18+ installé sur votre machine
- Des connaissances de base en React et TypeScript
- Une familiarité avec les transformations et transitions CSS (utile mais pas obligatoire)
- Un éditeur de code (VS Code recommandé)
Étape 1 : Configuration du projet
Créez un nouveau projet Next.js et installez Motion :
npx create-next-app@latest motion-demo --typescript --tailwind --app --src-dir
cd motion-demoInstallez la bibliothèque Motion :
npm install motionMotion v11+ est publié sous le nom de package motion (le package framer-motion fonctionne encore mais redirige vers motion). L'API est la même — seul le nom du package a changé.
Vérifiez votre installation en consultant package.json :
{
"dependencies": {
"motion": "^11.18.0",
"next": "^15.2.0",
"react": "^19.0.0"
}
}Étape 2 : Votre première animation
Remplacez le contenu de src/app/page.tsx par un composant animé simple :
"use client";
import { motion } from "motion/react";
export default function Home() {
return (
<main className="flex min-h-screen items-center justify-center bg-gray-950">
<motion.div
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
className="rounded-2xl bg-white p-8 shadow-2xl"
>
<h1 className="text-2xl font-bold text-gray-900">
Bonjour, Motion !
</h1>
<p className="mt-2 text-gray-600">
Cette carte est apparue en fondu et a glissé vers le haut.
</p>
</motion.div>
</main>
);
}Lancez le serveur de développement avec npm run dev et ouvrez http://localhost:3000. Vous verrez la carte apparaître en fondu et glisser vers le haut au chargement de la page.
Comment ça fonctionne
motion.divest un remplacement direct de<div>qui supporte les props d'animationinitialdéfinit l'état de départ (invisible, décalé de 40px vers le bas)animatedéfinit l'état cible (entièrement visible, position originale)transitioncontrôle le timing et l'accélération
Motion interpole automatiquement entre les valeurs initial et animate, gérant toute la logique requestAnimationFrame et les transformations accélérées par GPU pour vous.
Étape 3 : Physique des ressorts et types de transitions
Motion supporte trois types de transitions : tween (basé sur la durée), spring (basé sur la physique) et inertia (basé sur l'élan). Les animations à ressort sont les plus naturelles pour les interactions UI.
"use client";
import { motion } from "motion/react";
export default function SpringDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
{/* Tween : prévisible, basé sur la durée */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "tween", duration: 0.5 }}
className="h-24 w-24 rounded-2xl bg-blue-500"
/>
{/* Spring : naturel, basé sur la physique */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", stiffness: 260, damping: 20 }}
className="h-24 w-24 rounded-2xl bg-green-500"
/>
{/* Spring avec rebond */}
<motion.div
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: "spring", bounce: 0.5 }}
className="h-24 w-24 rounded-2xl bg-purple-500"
/>
</div>
);
}Paramètres du ressort
| Paramètre | Description | Défaut |
|---|---|---|
stiffness | Rigidité du ressort (plus élevé = plus vif) | 100 |
damping | Résistance (plus élevé = moins d'oscillation) | 10 |
mass | Poids de l'objet animé | 1 |
bounce | Raccourci : 0 = pas de rebond, 1 = très rebondissant | 0.25 |
Astuce : Pour la plupart des animations UI, type: "spring" avec les valeurs par défaut fonctionne parfaitement. Ne modifiez les paramètres que quand le rendu par défaut ne correspond pas à votre intention de design.
Étape 4 : Gestes interactifs
Motion rend triviale l'ajout d'animations de survol, clic, glissement et focus. Ces props de geste répondent aux interactions utilisateur sans aucune gestion d'état.
Survol et clic
"use client";
import { motion } from "motion/react";
export default function GestureDemo() {
return (
<div className="flex min-h-screen items-center justify-center gap-8 bg-gray-950">
<motion.button
whileHover={{ scale: 1.05, backgroundColor: "#3b82f6" }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
className="rounded-xl bg-blue-600 px-8 py-4 text-lg font-semibold text-white"
>
Cliquez-moi
</motion.button>
<motion.div
whileHover={{
rotate: 5,
boxShadow: "0 25px 50px -12px rgba(0,0,0,0.5)",
}}
whileTap={{ rotate: -5, scale: 0.9 }}
className="flex h-32 w-32 cursor-pointer items-center justify-center rounded-2xl bg-gradient-to-br from-pink-500 to-orange-400 text-white font-bold"
>
Interagir
</motion.div>
</div>
);
}Glissement
Motion supporte le glissement complet avec des contraintes, des limites élastiques et l'élan :
"use client";
import { motion } from "motion/react";
import { useRef } from "react";
export default function DragDemo() {
const constraintsRef = useRef<HTMLDivElement>(null);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950">
{/* Conteneur des limites de glissement */}
<motion.div
ref={constraintsRef}
className="relative flex h-96 w-96 items-center justify-center rounded-3xl border-2 border-dashed border-gray-700"
>
<p className="absolute top-4 text-sm text-gray-500">
Glissez la balle
</p>
<motion.div
drag
dragConstraints={constraintsRef}
dragElastic={0.2}
dragTransition={{ bounceStiffness: 300, bounceDamping: 20 }}
whileDrag={{ scale: 1.2, cursor: "grabbing" }}
className="h-20 w-20 cursor-grab rounded-full bg-gradient-to-br from-violet-500 to-cyan-400 shadow-lg"
/>
</motion.div>
</div>
);
}Props de glissement principales :
dragactive le glissement (true,"x"ou"y"pour verrouiller un axe)dragConstraintslimite le mouvement (ref ou valeurs en pixels)dragElasticcontrôle la distance au-delà des contraintes (0-1)whileDragapplique des styles pendant le glissement
Étape 5 : Variants et orchestration
Quand vous devez animer plusieurs enfants en séquence, les variants vous permettent de définir des états d'animation nommés et de les propager dans l'arbre des composants.
"use client";
import { motion } from "motion/react";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, y: 20, scale: 0.95 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
const features = [
{ title: "Rapide", description: "Animations accélérées par GPU à 60fps" },
{ title: "Simple", description: "API déclarative sans code standard" },
{ title: "Puissant", description: "Support des gestes, layout et défilement" },
{ title: "Léger", description: "Tree-shakeable, livrez uniquement ce que vous utilisez" },
];
export default function StaggerDemo() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="grid max-w-2xl grid-cols-2 gap-4"
>
{features.map((feature) => (
<motion.div
key={feature.title}
variants={itemVariants}
className="rounded-xl bg-gray-900 p-6 border border-gray-800"
>
<h3 className="text-lg font-bold text-white">{feature.title}</h3>
<p className="mt-1 text-sm text-gray-400">{feature.description}</p>
</motion.div>
))}
</motion.div>
</div>
);
}Comment les variants se propagent
- Le parent
motion.divdéfinitinitial="hidden"etanimate="visible" - Les enfants avec
variantshéritent automatiquement de ces états staggerChildren: 0.1ajoute un délai de 100ms entre chaque animation enfantdelayChildren: 0.2attend 200ms avant le premier enfant
Ce pattern est parfait pour animer des listes, grilles, menus de navigation et tout groupe d'éléments liés.
Étape 6 : AnimatePresence pour les animations de sortie
React supprime les éléments du DOM instantanément. AnimatePresence intercepte cela et permet aux animations de sortie de se terminer avant la suppression.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const notifications = [
{ id: 1, text: "Nouveau message reçu", color: "bg-blue-500" },
{ id: 2, text: "Fichier téléchargé avec succès", color: "bg-green-500" },
{ id: 3, text: "Paiement traité", color: "bg-purple-500" },
{ id: 4, text: "Déploiement terminé", color: "bg-orange-500" },
];
export default function ExitDemo() {
const [items, setItems] = useState(notifications);
const removeItem = (id: number) => {
setItems((prev) => prev.filter((item) => item.id !== id));
};
const resetItems = () => setItems(notifications);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="w-full max-w-md space-y-3">
<AnimatePresence mode="popLayout">
{items.map((item) => (
<motion.div
key={item.id}
layout
initial={{ opacity: 0, x: -40, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 40, scale: 0.95 }}
transition={{ type: "spring", stiffness: 350, damping: 25 }}
className="flex items-center justify-between rounded-xl bg-gray-900 p-4 border border-gray-800"
>
<div className="flex items-center gap-3">
<div className={`h-3 w-3 rounded-full ${item.color}`} />
<span className="text-white">{item.text}</span>
</div>
<button
onClick={() => removeItem(item.id)}
className="text-gray-500 hover:text-red-400 transition-colors"
>
Fermer
</button>
</motion.div>
))}
</AnimatePresence>
{items.length === 0 && (
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
onClick={resetItems}
className="w-full rounded-xl bg-blue-600 py-3 text-white font-semibold"
>
Réinitialiser les notifications
</motion.button>
)}
</div>
</div>
);
}Modes AnimatePresence
| Mode | Comportement |
|---|---|
"sync" (défaut) | Les animations de sortie et d'entrée se produisent simultanément |
"wait" | Attend la fin de la sortie avant d'entrer |
"popLayout" | Les éléments sortants sont retirés du flux de mise en page |
La prop layout sur chaque élément assure que les éléments restants se repositionnent en douceur quand un voisin est supprimé.
Étape 7 : Animations de mise en page
Les animations de mise en page sont l'une des fonctionnalités les plus puissantes de Motion. En ajoutant la prop layout, les éléments transitionnent en douceur entre différentes mises en page CSS — sans calculs manuels de coordonnées.
"use client";
import { useState } from "react";
import { motion } from "motion/react";
export default function LayoutDemo() {
const [selected, setSelected] = useState<string | null>(null);
const items = ["Design", "Développer", "Déployer", "Surveiller"];
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="flex gap-2 rounded-2xl bg-gray-900 p-2">
{items.map((item) => (
<button
key={item}
onClick={() => setSelected(item)}
className="relative rounded-xl px-6 py-3 text-sm font-medium text-white outline-none"
>
{selected === item && (
<motion.div
layoutId="active-tab"
className="absolute inset-0 rounded-xl bg-blue-600"
transition={{ type: "spring", stiffness: 380, damping: 30 }}
/>
)}
<span className="relative z-10">{item}</span>
</button>
))}
</div>
</div>
);
}La magie de layoutId
Quand vous donnez le même layoutId à plusieurs éléments, Motion les traite comme un seul élément à travers les rendus. Quand l'un est démonté et un autre monté, Motion anime entre leurs positions et tailles automatiquement. C'est ainsi que vous créez :
- Des transitions d'éléments partagés (comme l'indicateur d'onglet ci-dessus)
- Des expansions de carte vers modale
- Des animations de réordonnancement de liste
Exemple d'expansion de carte
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
const cards = [
{ id: "1", title: "Analytique", description: "Suivez le comportement des utilisateurs et les métriques d'engagement sur votre plateforme.", color: "from-blue-500 to-cyan-400" },
{ id: "2", title: "Sécurité", description: "Chiffrement de bout en bout et infrastructure prête pour la conformité.", color: "from-purple-500 to-pink-400" },
{ id: "3", title: "Vitesse", description: "Déployé globalement en edge avec des temps de réponse inférieurs à 100ms.", color: "from-orange-500 to-yellow-400" },
];
export default function CardExpansion() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedCard = cards.find((c) => c.id === selectedId);
return (
<div className="flex min-h-screen items-center justify-center bg-gray-950 p-8">
<div className="grid grid-cols-3 gap-4">
{cards.map((card) => (
<motion.div
key={card.id}
layoutId={`card-${card.id}`}
onClick={() => setSelectedId(card.id)}
className={`cursor-pointer rounded-2xl bg-gradient-to-br ${card.color} p-6`}
whileHover={{ scale: 1.03 }}
whileTap={{ scale: 0.98 }}
>
<motion.h3
layoutId={`title-${card.id}`}
className="text-xl font-bold text-white"
>
{card.title}
</motion.h3>
</motion.div>
))}
</div>
<AnimatePresence>
{selectedId && selectedCard && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 0.5 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 bg-black"
/>
<motion.div
layoutId={`card-${selectedId}`}
className={`fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md rounded-2xl bg-gradient-to-br ${selectedCard.color} p-8`}
>
<motion.h3
layoutId={`title-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedCard.title}
</motion.h3>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className="mt-4 text-white/90"
>
{selectedCard.description}
</motion.p>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 rounded-lg bg-white/20 px-4 py-2 text-white font-medium backdrop-blur"
>
Fermer
</motion.button>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}Cela crée une expansion fluide de la carte vers la modale sans aucun suivi manuel de position. Motion calcule automatiquement l'animation FLIP.
Étape 8 : Animations pilotées par le défilement
Motion fournit des hooks pour créer des animations qui répondent à la position de défilement. C'est parfait pour les sections héro, les indicateurs de progression, les effets de parallaxe et les patterns de révélation au défilement.
Indicateur de progression de défilement
"use client";
import { motion, useScroll, useSpring } from "motion/react";
export default function ScrollProgress() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, {
stiffness: 100,
damping: 30,
restDelta: 0.001,
});
return (
<>
<motion.div
style={{ scaleX }}
className="fixed top-0 left-0 right-0 z-50 h-1 origin-left bg-blue-500"
/>
<main className="space-y-8 p-8">
{Array.from({ length: 20 }).map((_, i) => (
<div
key={i}
className="mx-auto h-48 max-w-2xl rounded-2xl bg-gray-900 border border-gray-800"
/>
))}
</main>
</>
);
}Révélation au défilement
"use client";
import { motion, useInView } from "motion/react";
import { useRef } from "react";
function RevealSection({ children }: { children: React.ReactNode }) {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true, margin: "-100px" });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 60 }}
animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 60 }}
transition={{ duration: 0.7, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
export default function ScrollReveal() {
const sections = [
{ title: "Designer", text: "Créez de belles interfaces avec Motion" },
{ title: "Animer", text: "Ajoutez des animations fluides à chaque interaction" },
{ title: "Livrer", text: "Déployez des animations performantes en production" },
];
return (
<main className="min-h-screen bg-gray-950 p-8">
<div className="mx-auto max-w-2xl space-y-32 py-32">
{sections.map((section) => (
<RevealSection key={section.title}>
<h2 className="text-4xl font-bold text-white">{section.title}</h2>
<p className="mt-4 text-xl text-gray-400">{section.text}</p>
</RevealSection>
))}
</div>
</main>
);
}Effet de parallaxe
"use client";
import { motion, useScroll, useTransform } from "motion/react";
import { useRef } from "react";
export default function ParallaxHero() {
const ref = useRef<HTMLDivElement>(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start start", "end start"],
});
const y = useTransform(scrollYProgress, [0, 1], [0, 200]);
const opacity = useTransform(scrollYProgress, [0, 0.8], [1, 0]);
const scale = useTransform(scrollYProgress, [0, 1], [1, 0.85]);
return (
<div ref={ref} className="relative h-screen overflow-hidden bg-gray-950">
<motion.div
style={{ y, opacity, scale }}
className="flex h-full flex-col items-center justify-center"
>
<h1 className="text-6xl font-bold text-white">Héro Parallaxe</h1>
<p className="mt-4 text-xl text-gray-400">Défilez vers le bas pour voir l'effet</p>
</motion.div>
</div>
);
}Étape 9 : Construire une galerie de produits animée complète
Combinons tout dans un composant réel — une galerie de fiches produits animée avec une entrée décalée, des effets de survol et des vues détaillées extensibles.
"use client";
import { useState } from "react";
import { motion, AnimatePresence } from "motion/react";
interface Product {
id: string;
name: string;
price: string;
category: string;
gradient: string;
description: string;
}
const products: Product[] = [
{
id: "1",
name: "Casque Pro",
price: "299 €",
category: "Audio",
gradient: "from-violet-600 to-indigo-600",
description:
"Casque sans fil premium avec réduction de bruit active, 40 heures d'autonomie et son qualité studio.",
},
{
id: "2",
name: "Écran Ultra",
price: "899 €",
category: "Affichage",
gradient: "from-cyan-600 to-blue-600",
description:
"Écran 32 pouces 4K avec HDR1000, taux de rafraîchissement 165Hz et couleurs calibrées en usine pour les créatifs.",
},
{
id: "3",
name: "Clavier Mécanique",
price: "179 €",
category: "Saisie",
gradient: "from-orange-600 to-red-600",
description:
"Clavier mécanique hot-swap avec RGB par touche, montage gasket et touches PBT premium.",
},
{
id: "4",
name: "Souris Ergonomique",
price: "129 €",
category: "Saisie",
gradient: "from-emerald-600 to-teal-600",
description:
"Souris verticale ergonomique avec capteur 8K DPI, Bluetooth 5.3 et boutons latéraux personnalisables.",
},
];
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: { staggerChildren: 0.08, delayChildren: 0.1 },
},
};
const cardVariants = {
hidden: { opacity: 0, y: 30, scale: 0.96 },
visible: {
opacity: 1,
y: 0,
scale: 1,
transition: { type: "spring", stiffness: 300, damping: 24 },
},
};
export default function ProductGallery() {
const [selectedId, setSelectedId] = useState<string | null>(null);
const selectedProduct = products.find((p) => p.id === selectedId);
return (
<div className="min-h-screen bg-gray-950 p-8">
<motion.h1
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
className="mb-12 text-center text-4xl font-bold text-white"
>
Produits en vedette
</motion.h1>
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="mx-auto grid max-w-5xl grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-4"
>
{products.map((product) => (
<motion.div
key={product.id}
layoutId={`product-${product.id}`}
variants={cardVariants}
onClick={() => setSelectedId(product.id)}
whileHover={{ y: -8, transition: { type: "spring", stiffness: 400 } }}
whileTap={{ scale: 0.97 }}
className="cursor-pointer overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-40 bg-gradient-to-br ${product.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${product.id}`}
className="text-5xl"
>
{product.category === "Audio"
? "🎧"
: product.category === "Affichage"
? "🖥️"
: product.category === "Saisie"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-4">
<motion.h3
layoutId={`name-${product.id}`}
className="font-bold text-white"
>
{product.name}
</motion.h3>
<motion.span
layoutId={`price-${product.id}`}
className="mt-1 block text-sm text-gray-400"
>
{product.price}
</motion.span>
</div>
</motion.div>
))}
</motion.div>
<AnimatePresence>
{selectedId && selectedProduct && (
<>
<motion.div
key="backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="fixed inset-0 z-40 bg-black"
/>
<motion.div
key="modal"
layoutId={`product-${selectedId}`}
className="fixed z-50 top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-lg overflow-hidden rounded-2xl bg-gray-900 border border-gray-800"
>
<div
className={`h-48 bg-gradient-to-br ${selectedProduct.gradient} flex items-center justify-center`}
>
<motion.span
layoutId={`emoji-${selectedId}`}
className="text-7xl"
>
{selectedProduct.category === "Audio"
? "🎧"
: selectedProduct.category === "Affichage"
? "🖥️"
: selectedProduct.category === "Saisie"
? "⌨️"
: "🖱️"}
</motion.span>
</div>
<div className="p-6">
<motion.h3
layoutId={`name-${selectedId}`}
className="text-2xl font-bold text-white"
>
{selectedProduct.name}
</motion.h3>
<motion.span
layoutId={`price-${selectedId}`}
className="mt-1 block text-lg text-gray-400"
>
{selectedProduct.price}
</motion.span>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1, transition: { delay: 0.2 } }}
exit={{ opacity: 0 }}
className="mt-4 text-gray-300"
>
{selectedProduct.description}
</motion.p>
<motion.button
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0, transition: { delay: 0.3 } }}
exit={{ opacity: 0 }}
onClick={() => setSelectedId(null)}
className="mt-6 w-full rounded-xl bg-white/10 py-3 text-white font-semibold hover:bg-white/20 transition-colors"
>
Fermer
</motion.button>
</div>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}Cette galerie démontre :
- Entrée décalée avec
containerVariantsetcardVariants - Effet de levée au survol avec
whileHover - Transition de mise en page partagée de la carte à la modale avec
layoutId - Animation de sortie avec
AnimatePresence - Révélation de contenu retardée dans la modale étendue
Étape 10 : Optimisation des performances
Motion est conçu pour être performant, mais il existe des patterns pour garder vos animations fluides en production.
1. Animez uniquement Transform et Opacity
Les propriétés accélérées par GPU (transform et opacity) sont les moins coûteuses à animer. Évitez d'animer width, height, padding ou top/left — utilisez scale, x, y et opacity à la place.
// Lent : déclenche un recalcul de mise en page
<motion.div animate={{ width: 200, height: 200 }} />
// Rapide : transform accéléré par GPU
<motion.div animate={{ scale: 1.5 }} />2. Utilisez layout avec discernement
La prop layout est puissante mais coûteuse pour les grandes listes. Si vous n'avez besoin que de changements de position, utilisez layout="position" au lieu de layout={true} :
// Anime la position, la taille et le border-radius
<motion.div layout />
// Anime uniquement la position (moins coûteux)
<motion.div layout="position" />3. Réduisez le mouvement pour l'accessibilité
Respectez toujours la préférence de l'utilisateur pour le mouvement réduit :
"use client";
import { motion, useReducedMotion } from "motion/react";
export default function AccessibleAnimation() {
const shouldReduceMotion = useReducedMotion();
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0.01 : 0.6 }}
className="rounded-2xl bg-gray-900 p-8"
>
<p className="text-white">Respecte prefers-reduced-motion</p>
</motion.div>
);
}4. Chargement paresseux des composants Motion
Si les animations ne sont utilisées que sur des pages spécifiques, importez Motion dynamiquement pour réduire la taille du bundle :
import dynamic from "next/dynamic";
const AnimatedHero = dynamic(() => import("@/components/AnimatedHero"), {
ssr: false,
loading: () => <div className="h-screen bg-gray-950" />,
});5. Utilisez will-change avec parcimonie
Motion applique déjà will-change: transform quand c'est nécessaire. Ne l'ajoutez pas manuellement sauf si vous avez profilé et confirmé que cela aide — l'utilisation excessive consomme la mémoire GPU.
Dépannage
Problèmes courants
L'animation clignote au premier rendu avec Next.js App Router
Ajoutez "use client" à tout composant utilisant Motion. Les Server Components ne peuvent pas utiliser les hooks ou les props de geste de Motion.
L'animation de mise en page fait sauter les éléments
Assurez-vous que les éléments parents ont des dimensions stables. Évitez les largeurs en pourcentage sur les éléments avec layout — utilisez des tailles fixes ou basées sur flex à la place.
L'animation de sortie ne joue pas
Vérifiez que :
AnimatePresenceenveloppe les éléments conditionnels- Chaque enfant a une prop
keyunique - La prop
exitest définie sur l'enfant direct deAnimatePresence
La taille du bundle est trop grande
Motion v11+ est entièrement tree-shakeable. Importez uniquement ce dont vous avez besoin :
// Bien : importe uniquement ce qui est nécessaire
import { motion, AnimatePresence } from "motion/react";
// À éviter : tout importer
import * as Motion from "motion/react";Prochaines étapes
Maintenant que vous comprenez les fondamentaux de Motion, voici quelques directions à explorer :
- Transitions de page — combinez Motion avec les transitions de layout Next.js pour une navigation fluide
- Animations SVG — utilisez
motion.pathetpathLengthpour des effets de dessin - Transformations 3D — utilisez
rotateX,rotateYetperspectivepour des effets de retournement de carte - Parallaxe lié au défilement — construisez des landing pages immersives avec
useScrolletuseTransform - Hooks d'animation réutilisables — extrayez les patterns dans des hooks personnalisés comme
useFadeInouuseStagger
Conclusion
Motion apporte des animations de qualité production à React avec une API simple et déclarative. Vous avez appris à créer des animations de base avec des ressorts et des transitions, à construire des gestes interactifs pour le survol, le clic et le glissement, à orchestrer des séquences complexes avec des variants, à gérer les transitions d'entrée et de sortie avec AnimatePresence, à animer automatiquement les changements de mise en page et à construire des effets pilotés par le défilement.
La clé d'une excellente animation UI est la retenue — animez avec intention, respectez les préférences de mouvement réduit et optimisez toujours les performances. Commencez avec la galerie de produits que vous avez construite dans ce tutoriel et expérimentez en ajoutant Motion à vos propres projets.
Discutez de votre projet avec nous
Nous sommes ici pour vous aider avec vos besoins en développement Web. Planifiez un appel pour discuter de votre projet et comment nous pouvons vous aider.
Trouvons les meilleures solutions pour vos besoins.
Articles connexes

Construire une application full-stack en temps réel avec Convex et Next.js 15
Apprenez à construire une application full-stack en temps réel avec Convex et Next.js 15. Ce tutoriel couvre la conception de schémas, les requêtes, les mutations, les abonnements en temps réel, l'authentification et le téléchargement de fichiers — le tout avec une sécurité de types de bout en bout.

Construire une application complète avec Firebase et Next.js 15 : Auth, Firestore et temps réel
Apprenez à créer une application full-stack avec Next.js 15 et Firebase. Ce guide couvre l'authentification, Firestore, les mises à jour en temps réel, les Server Actions et le déploiement sur Vercel.

Authentifier votre application Next.js 15 avec Auth.js v5 : Email, OAuth et contrôle des rôles
Apprenez à ajouter une authentification prête pour la production à votre application Next.js 15 avec Auth.js v5. Ce guide complet couvre Google OAuth, les identifiants email/mot de passe, les routes protégées, le middleware et le contrôle d'accès basé sur les rôles.