framer-motion
SkillDev toolsFramer Motion / Motion sub-skill - AnimatePresence, layout animations, gestures, motion values.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the framer-motion skill
What this skill tells your AI
The instructions your AI receives, as published by athevon/genjutsu in skills/_jutsu/framer-motion/SKILL.md and read by ahel’s review.
Version-sensitive. Every API name, SDK gate and browser-support claim below was verified on 2026-09-08 against primary sources. What against, and when, is in
_jutsu/VERSIONS.md. If that date is old, re-verify before acting on a version number.
Framer Motion — Sub-skill
Two package names, one library. Framer Motion was renamed to Motion.
motionandframer-motionboth publish the same version (13.2.0 as of 2026-09-08):motiondeclares"framer-motion": "^13.2.0"as a dependency andmotion/reactre-exports it. Neither is broken, andframer-motionis still the one most installed projects have.Read
package.jsonand follow what is there. Do not migrate a project from one to the other unless the user asks - that is Iron Rule 8 incast/ 10 inpaint.
In package.jsonImport from Install line motion"motion/react"npm install motionframer-motion"framer-motion"npm install framer-motionboth whichever the file you are editing already imports; say the project is mid-migration - Every API in this skill is identical across the two names at v13. Where a symbol is newer than v11, it is flagged inline. Peers:
react/react-dom^18 || ^19.
When to use Framer Motion vs alternatives
| Criteria | Framer Motion | GSAP | Native CSS |
|---|---|---|---|
| Layout animations | Excellent (layoutId) | Manual | Impossible |
| Exit animations | AnimatePresence | Timeline reverse | Limited (display) |
| Gestures (drag, hover) | Native, declarative | Draggable plugin | Basic |
| Scroll-driven | useScroll + useTransform | ScrollTrigger (more powerful) | scroll-timeline |
| Complex orchestration | Variants + propagation | Timeline (more flexible) | @keyframes |
| Bundle size | ~50kb tree-shaken | ~30kb core | 0kb |
| React integration | Native, component-first | Refs + useGSAP | className toggle |
Rule: Framer Motion for React UI interactions (modals, toasts, reorder, shared layout). GSAP for complex timelines, cinematic scroll-driven, SVG morphing.
AnimatePresence — Exit animations
<AnimatePresence mode="wait">
{isVisible && (
<motion.div
key="unique-key" // REQUIRED — identifies the component
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence>
mode="wait"— waits for exit to finish before enter (page transitions)mode="sync"— exit and enter simultaneouslymode="popLayout"— removes from flow immediately (good for lists)onExitComplete— callback when all exit animations are finished
Layout animations
// Shared layout — the element "slides" between two positions
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
// Auto layout — animates position/size when layout changes
<motion.div layout>
{isExpanded && <motion.p layout>Additional content</motion.p>}
</motion.div>
// layout="position" — animates position only (not size)
// layout="size" — animates size only
// layout="preserve-aspect" — preserves the ratio during the transition
Variants — Propagation and orchestration
import { stagger } from "motion/react";
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
// staggerChildren + staggerDirection are DEPRECATED (Motion 12.22, Jul 2025).
// Pass a stagger() function to delayChildren instead.
delayChildren: stagger(0.08, { startDelay: 0.2 }),
// reverse order: stagger(0.08, { from: "last" })
// also available: from: "first" | "center" | "last" | index, and ease
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item} />
))}
</motion.ul>
Variants automatically propagate to motion children — no need for initial/animate on children.
Gestures
<motion.div
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
whileFocus={{ borderColor: "#3b82f6" }}
// Drag
drag // true = x+y, "x" = horizontal only, "y" = vertical only
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // 0 = rigid, 1 = free (default 0.35)
dragSnapToOrigin // returns to initial position
onDragEnd={(e, info) => {
if (info.offset.x > 100) handleSwipe("right");
}}
/>
Motion values — Reactive without re-render
const x = useMotionValue(0);
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0]);
const background = useTransform(x, [-200, 200], ["#ff0000", "#00ff00"]);
// Spring-based smoothing
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
// Scroll tracking
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
// Element-scoped scroll
const ref = useRef(null);
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"],
});
Motion values do NOT trigger React re-renders — they update the DOM directly via style.
Do Not
Do not use motion with styled-components / Emotion without isValidProp (v13+)
Motion 13.0 removed @emotion/is-prop-valid as an optional dependency. Without explicit injection, motion-only props leak onto the DOM.
import isPropValid from "@emotion/is-prop-valid";
import { MotionConfig } from "motion/react";
<MotionConfig isValidProp={isPropValid}>
<App />
</MotionConfig>
Or reverse the composition so the styling library owns prop forwarding: const MotionDiv = motion.create(StyledDiv).
Do not setState in callbacks without a guard
// BAD — infinite re-render if animate depends on state
onUpdate={(latest) => setPosition(latest.x)}
// GOOD — guard or useMotionValueEvent
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});
Do not use layout animation without a stable key
// BAD — key changes every render, breaks layout tracking
<motion.div layout key={Math.random()} />
// GOOD — stable key derived from data
<motion.div layout key={item.id} />
Do not forget the unique key on AnimatePresence
// BAD — no key, exit animation does not trigger
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
// GOOD — unique key for each conditional child
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>
Do not wrap an already animated component with motion.div
// BAD — double animation, transform conflicts
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>Content</motion.div>
</motion.div>
// GOOD — single animation level per transform axis
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
</motion.div>
// GOOD — use variants to coordinate parent/child
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>
Signals
- GitHub stars
- 348
- Forks
- 26
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
framer-motion- Source
- github.com/athevon/genjutsu