Motion Foundations
La couche de base du système d'animation. Définit chaque valeur, contrainte et règle que les compétences downstream (motion-patterns, motion-advanced) héritent. Chargez cette compétence avant tout travail d'animation.
Quand activer
- Créer un composant animé à partir de zéro
- Configurer des jetons, préréglages de ressort ou valeurs d'easing
- Implémenter le support de
prefers-reduced-motion - Déboguer des erreurs d'hydratation dues aux états initiaux d'animation
- Évaluer si une animation doit exister ou non
Livrables
Cette compétence produit :
- Un objet
motionTokenspartagé (durée, easing, distance, échelle) - Une carte de préréglages
springspartagée (5 configurations nommées) - Une porte
shouldAnimate()utilisée par tous les composants - Des valeurs par défaut d'animation conformes à l'accessibilité via
useReducedMotion - Des états initiaux compatibles SSR sans avertissements d'hydratation
Principes
Une animation doit remplir au moins l'une des conditions suivantes, sinon elle doit être supprimée :
- Guider l'attention
- Communiquer un état
- Préserver la continuité spatiale
La réactivité prime toujours sur la fluidité. Une animation à 60 fps qui provoque un délai de saisie est pire qu'aucune animation.
Règles
Ces règles sont non négociables. Elles s'appliquent à chaque composant du système.
- Utilisez
motion/reactuniquement. N'importez jamais depuisframer-motion. Ne mélangez jamais les deux dans le même arbre. initialdoit correspondre à la sortie du serveur. Si le serveur rendopacity: 1, la propinitialdoit aussi êtreopacity: 1. Aucune exception.- Le mouvement réduit écrase tout. Quand
useReducedMotion()retournetrueou queprefersReducedesttrue, toutes les transformations sont désactivées. Les fondus uniquement en opacité d'au maximum 0,2 seconde sont la seule solution de repli autorisée. - N'animez jamais les propriétés de layout.
width,height,top,left,margin,paddingsont interdits dansanimate. Utilisez uniquementtransformetopacity. - Toutes les valeurs de jetons proviennent de
motionTokens. Les durées et easings codés en dur dans les fichiers de composants sont interdits. - Toutes les configurations de ressort proviennent de la carte
springs. Les valeursstiffness/dampingen ligne sont interdites. "use client"est obligatoire sur chaque fichier qui importe depuismotion/react.- Ne lisez jamais
windowounavigatorau niveau du module. Protégez toujours avectypeof window !== "undefined".
Guide de décision
Choisir une durée
| Jeton | Utilisation |
|---|---|
instant | Affichage/masquage d'infobulle, anneau de focus, mise à jour de badge |
fast | Retour de bouton, changement d'icône, bascule de chip |
normal | Ouverture de modale, expansion de carte, entrée d'élément de page |
slow | Entrée de héros, transition de page entière |
crawl | Narration délibérée ; à utiliser avec parcimonie |
Choisir un ressort
| Préréglage | Utilisation |
|---|---|
snappy | UI par défaut — boutons, chips, éléments de navigation |
gentle | Cartes, modales, panneaux qui atterrissent en douceur |
bouncy | Moments ludiques — états vides, onboarding |
instant | Infobulles, popovers, menus déroulants |
release | Relâchement après glisser — sensation physique naturelle |
Quand désactiver complètement l'animation
Désactivez (faites en sorte que shouldAnimate() retourne false) quand :
prefersReducedesttrueisLowEndesttrueet l'animation n'est pas essentielle- L'élément est hors écran et n'entrera jamais dans la zone d'affichage
- L'animation est purement décorative sans but UX
Concepts fondamentaux
Système de jetons
// lib/motion-tokens.ts
export const motionTokens = {
duration: {
instant: 0.08,
fast: 0.18,
normal: 0.35,
slow: 0.6,
crawl: 1.0,
},
easing: {
smooth: [0.22, 1, 0.36, 1],
sharp: [0.4, 0, 0.2, 1],
bounce: [0.34, 1.56, 0.64, 1],
linear: [0, 0, 1, 1],
},
distance: {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 48,
},
scale: {
subtle: 0.98,
press: 0.95,
pop: 1.04,
},
}
export const springs = {
snappy: { type: "spring", stiffness: 300, damping: 30 },
gentle: { type: "spring", stiffness: 120, damping: 14 },
bouncy: { type: "spring", stiffness: 400, damping: 10 },
instant: { type: "spring", stiffness: 600, damping: 35 },
release: { type: "spring", stiffness: 200, damping: 20, restDelta: 0.001 },
}
Indicateurs d'exécution
// lib/motion-config.ts
export const motionConfig = {
isLowEnd() {
return (
typeof navigator !== "undefined" &&
navigator.hardwareConcurrency <= 4
)
},
prefersReduced() {
return (
typeof window !== "undefined" &&
window.matchMedia("(prefers-reduced-motion: reduce)").matches
)
},
shouldAnimate({ essential = false } = {}) {
if (this.prefersReduced()) return false
if (!essential && this.isLowEnd()) return false
return true
},
duration() {
return this.isLowEnd() || this.prefersReduced()
? motionTokens.duration.instant
: motionTokens.duration.normal
},
}
Accessibilité
Ordre de priorité (du plus élevé au plus bas) :
prefers-reduced-motion: reduce— désactive toutes les transformations, limite les transitions d'opacité à ≤ 0,2 s- Détection d'appareil bas de gamme — réduit la durée, supprime les animations non essentielles
- Préférence de conception — tout le reste
L'animation doit se dégrader gracieusement. Elle ne doit jamais disparaître brusquement d'une manière qui provoque un changement de layout ou désoriente l'utilisateur.
// hooks/use-reduced-motion.tsx
"use client"
import { useReducedMotion } from "motion/react"
export function useSafeMotion(fullY: number = 16) {
const reduce = useReducedMotion()
return {
initial: { opacity: 0, y: reduce ? 0 : fullY },
animate: { opacity: 1, y: 0 },
exit: { opacity: 0, y: reduce ? 0 : -fullY },
}
}
/* globals.css */
@media (prefers-reduced-motion: reduce) {
.motion-safe-transition { transition: opacity 0.15s; }
.motion-reduce-transform { transform: none !important; }
}
<!-- Tailwind -->
<div class="motion-safe:animate-fade motion-reduce:opacity-100"></div>
Sécurité SSR / hydratation
Règle : initial doit toujours correspondre à ce que le serveur rend.
// FAUX — le serveur rend opacity:1 mais initial dit 0 → erreur d'hydratation
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECT — utilisez AnimatePresence ou différez le montage client
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Exemples de code
De bout en bout : jetons + ressorts + accessibilité + garde SSR
// components/fade-in-card.tsx
"use client"
import { useState, useEffect } from "react"
import { motion } from "motion/react"
import { motionTokens, springs } from "@/lib/motion-tokens"
import { useSafeMotion } from "@/hooks/use-reduced-motion"
import { motionConfig } from "@/lib/motion-config"
interface FadeInCardProps {
children: React.ReactNode
delay?: number
}
export function FadeInCard({ children, delay = 0 }: FadeInCardProps) {
// Garde SSR — initial doit correspondre à la sortie du serveur (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accessibilité — désactive la transformation quand le mouvement réduit est préféré
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Porte matérielle — ignore l'animation sur le matériel bas de gamme
if (!motionConfig.shouldAnimate() || !mounted) {
return <div>{children}</div>
}
return (
<motion.div
initial={safeMotion.initial}
animate={safeMotion.animate}
exit={safeMotion.exit}
transition={{
...springs.gentle,
delay,
}}
whileHover={{ scale: motionTokens.scale.pop }}
whileTap={{ scale: motionTokens.scale.press }}
>
{children}
</motion.div>
)
}
Contraintes / Non-objectifs
Cette compétence ne couvre pas :
- Les motifs de composants UI (bouton, modale, stagger) → voir
motion-patterns - Le glisser, les gestes, SVG, les animations de texte, les hooks personnalisés → voir
motion-advanced - Les animations CSS uniquement ou les classes Tailwind
animate-*sansmotion/react - Les bibliothèques d'animation tierces (GSAP, anime.js, etc.)
- Les décisions de design d'animation (quand animer, quoi mettre en valeur) — c'est une préoccupation de design, pas une contrainte de code
Anti-patrons
| Anti-patron | Règle violée | Correctif |
|---|---|---|
import { motion } from "framer-motion" | Règle 1 | Utilisez motion/react |
initial={{ opacity: 0 }} sur un composant SSR | Règle 2 | Ajoutez une garde de montage |
Ignorer la vérification useReducedMotion | Règle 3 | Utilisez le hook useSafeMotion |
animate={{ width: "100%" }} | Règle 4 | Utilisez la transformation scaleX à la place |
transition={{ duration: 0.4 }} en ligne | Règle 5 | Utilisez motionTokens.duration.normal |
{ stiffness: 300, damping: 30 } en ligne | Règle 6 | Utilisez springs.snappy |
Directive "use client" manquante | Règle 7 | Ajoutez-la en haut du fichier |
navigator.hardwareConcurrency au niveau du module | Règle 8 | Enveloppez dans typeof navigator !== "undefined" |
Compétences associées
motion-patterns— consomme les jetons et ressorts définis ici pour construire des motifs de bouton, modale, stagger, transition de page et défilement. Ne redéfinit aucune valeur.motion-advanced— consomme les jetons et ressorts définis ici pour les motifs de glisser, SVG, texte et gestes. Ajoute des séquencesuseAnimateet des hooks personnalisés par-dessus cette fondation.