skills/community/motion-react/SKILL.md
Build React animations with Motion (formerly Framer Motion) - gestures (drag, hover, tap), scroll effects, spring physics, layout animations, SVG, exit animations, and motion values. Use when: building React animations, adding hover/tap/drag interactions, scroll-triggered effects, layout transitions, shared element animations, exit animations with AnimatePresence, or working with motion values and springs. Triggers: "animate", "motion component", "framer motion", "gesture", "drag", "scroll animation", "layout animation", "exit animation", "spring", "whileHover", "whileTap", "whileInView", "AnimatePresence", "layoutId", "useScroll", "useSpring", "useAnimate", "motion value", "reorder", "parallax".
npx skillsauth add pedronauck/skills motion-reactInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Package: motion (formerly framer-motion). Import from "motion/react".
pnpm add motion
// Standard React (Vite, CRA, Pages Router)
import { motion, AnimatePresence } from "motion/react"
// Next.js App Router — use "motion/react-client" for RSC tree-shaking
"use client"
import * as motion from "motion/react-client"
// Minimal bundle (2.3 KB) — imperative API only
import { useAnimate } from "motion/react-mini"
// Reduced bundle (4.6 KB) — LazyMotion + m component
import { LazyMotion, domAnimation, m } from "motion/react"
Every HTML/SVG element has a motion counterpart:
<motion.div />
<motion.button />
<motion.svg />
<motion.circle />
Custom components: wrap with motion.create():
const MotionBox = motion.create(Box)
// forwardRef required — the ref must reach a DOM node
<motion.div
initial={{ opacity: 0, y: 20 }} // mount state (or false to skip)
animate={{ opacity: 1, y: 0 }} // target state
exit={{ opacity: 0, y: -20 }} // unmount state (needs AnimatePresence)
transition={{ type: "spring", bounce: 0.25 }}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#00f" }}
whileDrag={{ scale: 1.1 }}
whileInView={{ opacity: 1 }}
viewport={{ once: true, margin: "-100px" }}
/>
Motion animates any CSS value: opacity, filter, background-image, mask-image.
Independent transforms (not possible in CSS alone):
x, y, zscale, scaleX, scaleYrotate, rotateX, rotateY, rotateZskewX, skewYoriginX, originY, originZValue types: numbers, strings with units ("100px"), colors (hex/rgba/hsla), "auto" for width/height.
Hardware acceleration: set transform directly for GPU compositing:
<motion.li
initial={{ transform: "translateX(-100px)" }}
animate={{ transform: "translateX(0px)" }}
transition={{ type: "spring" }}
/>
Pass arrays to animate through a sequence:
<motion.div animate={{ x: [0, 100, 0] }} />
// null = "use current value"
<motion.div animate={{ x: [null, 100, 0] }} />
Named animation states for orchestrated animations:
const list = {
visible: {
transition: { staggerChildren: 0.1 }
},
hidden: {}
}
const item = {
visible: { opacity: 1, y: 0 },
hidden: { opacity: 0, y: 20 }
}
<motion.ul initial="hidden" animate="visible" variants={list}>
<motion.li variants={item} />
<motion.li variants={item} />
</motion.ul>
Variants propagate through the tree. Children inherit animate/initial/exit from parent.
import { AnimatePresence } from "motion/react"
<AnimatePresence>
{isVisible && (
<motion.div
key="modal" // REQUIRED: unique key
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
Critical rules:
key propsexit prop only works on motion components inside AnimatePresence// WRONG — AnimatePresence unmounts with condition
{show && <AnimatePresence><motion.div /></AnimatePresence>}
// CORRECT — condition inside AnimatePresence
<AnimatePresence>{show && <motion.div key="k" />}</AnimatePresence>
Modes: "sync" (default), "wait" (sequential enter/exit), "popLayout" (pop exiting element out of flow).
Slideshow pattern — change key to trigger exit+enter:
<AnimatePresence mode="wait">
<motion.img
key={image.src}
initial={{ x: 300, opacity: 0 }}
animate={{ x: 0, opacity: 1 }}
exit={{ x: -300, opacity: 0 }}
/>
</AnimatePresence>
Dynamic exit data — pass via custom prop + usePresenceData:
<AnimatePresence custom={direction}>
<Slide key={id} />
</AnimatePresence>
// Inside Slide:
const direction = usePresenceData()
For full transition API details, see references/transitions-api.md.
Quick reference:
// Spring (default for physical props: x, y, scale)
transition={{ type: "spring", bounce: 0.25 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
transition={{ type: "spring", visualDuration: 0.5, bounce: 0.25 }}
// Tween (default for opacity, color)
transition={{ duration: 0.3, ease: "easeInOut" }}
// Per-value transitions
transition={{
default: { type: "spring" },
opacity: { duration: 0.2, ease: "linear" }
}}
// Orchestration
transition={{ delay: 0.5, repeat: Infinity, repeatType: "reverse" }}
// Global default
<MotionConfig transition={{ duration: 0.3 }}>
For full layout animation details, see references/layout-animations.md.
// Auto-animate any layout change
<motion.div layout />
// Shared element transitions
<motion.div layoutId="underline" />
// Customize layout transition
<motion.div layout transition={{ layout: { duration: 0.3 } }} />
For full gesture/drag API, see references/gestures-and-drag.md.
<motion.div
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
drag // enable both axes
drag="x" // constrain to x-axis
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.2}
/>
For full scroll API, see references/scroll-animations.md.
// Viewport-triggered
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
/>
// Scroll-linked progress bar
const { scrollYProgress } = useScroll()
<motion.div style={{ scaleX: scrollYProgress }} />
// Element scroll progress
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
})
For full hooks API, see references/hooks-and-motion-values.md.
// Manual motion values (no re-renders)
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
<motion.div drag="x" style={{ x, opacity }} />
// Smooth spring following
const springX = useSpring(x, { stiffness: 100, damping: 30 })
// Imperative animation control
const [scope, animate] = useAnimate()
animate("li", { opacity: 1 }, { stagger: 0.1 })
// Event listener (no re-render)
useMotionValueEvent(scrollY, "change", (v) => console.log(v))
| Approach | Size | What you get |
|----------|------|-------------|
| motion/react | ~34 KB | Full API |
| LazyMotion + m | ~4.6 KB | Declarative animations, no gestures |
| motion/react-mini | ~2.3 KB | useAnimate only |
// LazyMotion pattern
import { LazyMotion, domAnimation, m } from "motion/react"
<LazyMotion features={domAnimation}>
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
<MotionConfig reducedMotion="user">
<App />
</MotionConfig>
Options: "user" (respect OS setting), "always" (force instant), "never" (ignore).
Hook: useReducedMotion() returns true when user prefers reduced motion.
Let each library handle its strength. Remove Tailwind transition-* classes — they conflict.
// WRONG — Tailwind transition conflicts with Motion
<motion.div className="transition-all duration-300" animate={{ x: 100 }} />
// CORRECT — Tailwind for styling, Motion for animation
<motion.div className="rounded-lg bg-blue-600 p-4" whileHover={{ scale: 1.05 }} />
Motion components require client-side rendering. Use "motion/react-client" for optimal tree-shaking:
// components/motion-client.tsx
"use client"
import * as motion from "motion/react-client"
export { motion }
// app/page.tsx (Server Component)
import { motion } from "@/components/motion-client"
<motion.div animate={{ opacity: 1 }} />
keytransition-* classes from motion elementsheight: "auto" + display: "none" — Use visibility: "hidden" insteadlayoutScroll prop to scroll parentlayoutRoot prop to fixed parentpopLayout mode — Custom components must use forwardRef to forward ref to DOM nodepropagate — Set to true on nested AnimatePresence to fire child exitstools
Plans real-user QA deliverables: personas, journey maps, exploratory charters, persona/journey/tour/CFR test cases, regression suites, Figma validation checks, automation intent, and user-impact bug reports. Writes artifacts under <qa-output-path>/qa/ for qa-execution to consume. Use when planning QA before execution, documenting journey-driven test strategy, marking flows that need E2E follow-up, or filing structured bug reports. Do not use for live execution, AI implementation audits, CI gate ownership, or technical integration/security/performance suites; use qa-execution or agent-output-audit instead.
development
Executes real-user QA sessions through public interfaces using personas, journeys, exploratory charters, test tours, edge-case probes, CFR checks, and browser evidence. Reads qa-report artifacts from <qa-output-path>/qa/ when present, captures issues/screenshots/reports under the same output tree, and classifies bugs by user impact. Use when validating a release candidate, migration, refactor, or user-facing change against production-like behavior. Do not use for AI implementation audits, task-status reconciliation, CI gate runs, integration/security/performance templates, or flaky-test triage; use agent-output-audit for those.
development
Transform outside-of-diff review files into properly formatted issue files for a given PR. Use when converting review files from ai-docs/reviews-pr-<PR>/outside/ into issue format in ai-docs/reviews-pr-<PR>/issues/. Automatically determines starting issue number and preserves all metadata (file path, date, status) from original review files. Don't use for inline-diff review files, non-PR review artifacts, or creating GitHub issues directly.
development
Enforce root-cause fixes over workarounds, hacks, and symptom patches in all software engineering tasks. Use when debugging issues, fixing bugs, resolving test failures, planning solutions, making architectural decisions, or reviewing code changes. Activates gate functions that detect and reject common workaround patterns such as type assertions, lint suppressions, error swallowing, timing hacks, and monkey patches. Don't use for trivial formatting changes or documentation-only edits.