motion-foundations
affaan-m/ECC
Jetons de mouvement, préréglages de ressort, règles de performance, adaptation aux appareils, mise en œuvre de l'accessibilité et sécurité SSR pour React / Next.js à l'aide de motion/react. Couche de base — toutes les autres compétences en matière de mouvement reposent sur celle-ci.
...Développer toutPrincipes fondamentaux du mouvement
La couche de base du système de mouvement. Définit toutes les valeurs, contraintes et
règles dont héritent les compétences en aval (motion-patterns, motion-advanced) héritent.
Chargez cette compétence avant de commencer tout travail d’animation.
Quand l'activer
- Lorsque vous créez un composant animé à partir de zéro
- Configurer des jetons, des préréglages de ressort ou des valeurs d’accélération/décélération
- Mise en œuvre
prefers-reduced-motionla prise en charge - Déboguer les incohérences d'hydratation par rapport aux états initiaux de l'animation
- Évaluer si une animation doit réellement exister
Résultats
Cette compétence génère :
- Un objet partagé
motionTokens(durée, accélération, distance, échelle) - Une
springscarte de préréglages partagée (5 configurations nommées) - Une
shouldAnimate()porte utilisée par tous les composants - Paramètres d’animation par défaut conformes aux normes d’accessibilité via
useReducedMotion - des états initiaux compatibles avec le SSR, sans aucun avertissement 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 images par seconde qui entraîne un retard de réponse est pire que l'absence d'animation.
Règles
Elles sont non négociables. Elles s’appliquent à tous les composants du système.
- Utilisez uniquement
motion/react. N’importez jamais depuisframer-motion. Ne mélangez jamais les deux dans la même arborescence. initialdoit correspondre à la sortie du serveur. Si le serveur afficheopacity: 1, lainitialpropriété doit également êtreopacity: 1. Aucune exception.- La réduction des mouvements prime sur tout le reste. Lorsque
useReducedMotion()renvoietrueouprefersReducedesttrue, toutes les transformations sont désactivées. Les fondus basés uniquement sur l’opacité d’une durée ≤ 0,2 s constituent la seule solution de repli autorisée. - N’animez jamais les propriétés de mise en page.
width,height,top,left,margin,paddingsont interdites dansanimate. Utiliseztransformetopacityuniquement. - Toutes les valeurs des jetons proviennent de
motionTokens. Les durées et les accélérations codées en dur dans les fichiers de composants sont interdites. - Toutes les configurations Spring proviennent de la carte
springs. Lesstiffness/dampingsont interdites. "use client"est obligatoire dans chaque fichier qui importe depuismotion/react.- Ne lisez jamais
windowounavigatorau niveau du module. Utilisez toujours la protectiontypeof window !== "undefined".
Conseils de décision
Choix d'une durée
Choix d'un ressort
Quand désactiver complètement l’animation
Désactiver (rendre shouldAnimate() retour false) lorsque :
prefersReducedesttrueisLowEndesttrueet que l’animation n’est pas essentielle- L'élément est hors écran et n'apparaîtra jamais dans la fenêtre d'affichage
- L’animation est purement décorative et n’a aucune utilité en termes d’expérience utilisateur
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 des appareils bas de gamme — réduit la durée, supprime les animations non essentielles
- Préférences de conception — tout le reste
Le mouvement doit se dégrader en douceur. Il ne doit jamais disparaître brusquement d’une manière qui provoque un décalage de mise en page ou perturbe l’orientation.
// 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; }
}
<div class="motion-safe:animate-fade motion-reduce:opacity-100">div>
Sécurité SSR / hydratation
Règle : l’initiale doit toujours correspondre à ce que le serveur affiche.
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
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) {
// SSR guard — initial must match server output (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accessibility — disables transform when reduced motion is preferred
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Device gate — skip animation on low-end hardware
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 / Objectifs non visés
Cette compétence ne couvre pas :
- Les modèles de composants d’interface utilisateur (bouton, modal, stagger) → voir
motion-patterns - Glisser-déposer, gestes, SVG, animations de texte, hooks personnalisés → voir
motion-advanced - Animations uniquement en CSS ou classes
animate-*sansmotion/react - bibliothèques d’animation tierces (GSAP, anime.js, etc.)
- Décisions de conception du mouvement (quand animer, quoi mettre en valeur) — il s’agit d’une question de conception, et non d’une contrainte de code
Anti-modèles
Compétences associées
motion-patterns— utilise les jetons et les ressorts définis ici pour créer des modèles de boutons, de fenêtres modales, d’effets de décalage, de transitions de page et de défilement. Ne redéfinit aucune valeur.motion-advanced— utilise les jetons et les ressorts définis ici pour les effets de glissement, SVG, de texte et de gestes. AjouteuseAnimatedes séquences et des hooks personnalisés en complément de cette base.
Motion Foundations
The base layer of the motion system. Defines every value, constraint, and
rule that downstream skills (motion-patterns, motion-advanced) inherit.
Load this skill before any animation work begins.
When to Activate
- Starting any animated component from scratch
- Setting up tokens, spring presets, or easing values
- Implementing
prefers-reduced-motionsupport - Debugging hydration mismatches from animation initial states
- Evaluating whether an animation should exist at all
Outputs
This skill produces:
- A shared
motionTokensobject (duration, easing, distance, scale) - A shared
springspreset map (5 named configs) - A
shouldAnimate()gate used by all components - Accessibility-compliant animation defaults via
useReducedMotion - SSR-safe initial states with zero hydration warnings
Principles
Motion must do at least one of the following or it must be removed:
- Guide attention
- Communicate state
- Preserve spatial continuity
Responsiveness always outranks smoothness. A 60 fps animation that causes input delay is worse than no animation.
Rules
These are non-negotiable. They apply to every component in the system.
- Use
motion/reactonly. Never import fromframer-motion. Never mix the two in the same tree. initialmust match server output. If the server rendersopacity: 1, theinitialprop must also beopacity: 1. No exceptions.- Reduced motion overrides everything. When
useReducedMotion()returnstrueorprefersReducedistrue, all transforms are disabled. Opacity-only fades at ≤ 0.2s are the only permitted fallback. - Never animate layout properties.
width,height,top,left,margin,paddingare banned fromanimate. Usetransformandopacityonly. - All token values come from
motionTokens. Hardcoded durations and easings in component files are forbidden. - All spring configs come from the
springsmap. Inlinestiffness/dampingvalues are forbidden. "use client"is required on every file that imports frommotion/react.- Never read
windowornavigatorat module level. Always guard withtypeof window !== "undefined".
Decision Guidance
Choosing a duration
Choosing a spring
When to disable animation entirely
Disable (make shouldAnimate() return false) when:
prefersReducedistrueisLowEndistrueand the animation is non-essential- The element is off-screen and will never enter the viewport
- The animation is purely decorative with no UX purpose
Core Concepts
Token system
// 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 },
}
Runtime flags
// 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
},
}
Accessibility
Priority order (highest to lowest):
prefers-reduced-motion: reduce— disables all transforms, limits opacity transitions to ≤ 0.2s- Low-end device detection — reduces duration, removes non-essential animations
- Design preference — everything else
Motion must degrade gracefully. It must never disappear abruptly in a way that causes layout shift or confuses orientation.
// 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>
SSR / hydration safety
Rule: initial must always match what the server renders.
// WRONG — server renders opacity:1 but initial says 0 → hydration mismatch
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} />
// CORRECT — use AnimatePresence or defer to client mount
"use client"
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
<motion.div
initial={{ opacity: mounted ? 0 : 1 }}
animate={{ opacity: 1 }}
/>
Code Examples
End-to-end: tokens + springs + accessibility + SSR guard
// 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) {
// SSR guard — initial must match server output (opacity: 1)
const [mounted, setMounted] = useState(false)
useEffect(() => setMounted(true), [])
// Accessibility — disables transform when reduced motion is preferred
const safeMotion = useSafeMotion(motionTokens.distance.md)
// Device gate — skip animation on low-end hardware
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>
)
}
Constraints / Non-Goals
This skill does not cover:
- UI component patterns (button, modal, stagger) → see
motion-patterns - Drag, gestures, SVG, text animations, custom hooks → see
motion-advanced - CSS-only animations or Tailwind
animate-*classes withoutmotion/react - Third-party animation libraries (GSAP, anime.js, etc.)
- Motion design decisions (when to animate, what to emphasize) — that is a design concern, not a code constraint
Anti-Patterns
Related Skills
motion-patterns— consumes tokens and springs defined here to build button, modal, stagger, page transition, and scroll patterns. Does not redefine any values.motion-advanced— consumes tokens and springs defined here for drag, SVG, text, and gesture patterns. AddsuseAnimatesequences and custom hooks on top of this foundation.
Installer motion-foundations
Téléchargez et décompressez les fichiers de compétences dans votre répertoire .claude/skills/.
Télécharger le ZIPClonez le dépôt et copiez les fichiers de compétence dans votre projet.
git clone https://github.com/affaan-m/ECC/tree/main/skills/motion-foundations # Copy the skill folder to .claude/skills/ or .codex/skills/
Copier





Maison
