| name | framer-motion |
| description | Framer Motion / Motion sub-skill - AnimatePresence, layout animations, gestures, motion values. |
Framer Motion — Sub-skill
Package: motion (v11+, formerly framer-motion). Import: import { motion, AnimatePresence } from "motion/react"
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 simultaneously
mode="popLayout" — removes from flow immediately (good for lists)
onExitComplete — callback when all exit animations are finished
Layout animations
<motion.div layoutId="highlight" className={activeTab === id ? "active" : ""} />
<motion.div layout>
{isExpanded && <motion.p layout>Additional content</motion.p>}
</motion.div>
Variants — Propagation and orchestration
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.08,
delayChildren: 0.2,
staggerDirection: 1,
},
},
};
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
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
dragSnapToOrigin
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"]);
const smoothX = useSpring(x, { stiffness: 300, damping: 30 });
const { scrollY, scrollYProgress } = useScroll();
const parallaxY = useTransform(scrollYProgress, [0, 1], [0, -300]);
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 setState in callbacks without a guard
onUpdate={(latest) => setPosition(latest.x)}
const x = useMotionValue(0);
useMotionValueEvent(x, "change", (latest) => {
if (latest > threshold) onThresholdReached();
});
Do not use layout animation without a stable key
<motion.div layout key={Math.random()} />
<motion.div layout key={item.id} />
Do not forget the unique key on AnimatePresence
<AnimatePresence>
{isOpen && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>
<AnimatePresence>
{isOpen && <motion.div key="modal" exit={{ opacity: 0 }} />}
</AnimatePresence>
Do not wrap an already animated component with motion.div
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ x: -50 }}>Content</motion.div>
</motion.div>
<motion.div animate={{ x: 100 }}>
<motion.div animate={{ opacity: 0.5 }}>Content</motion.div>
</motion.div>
<motion.div variants={parent} animate="active">
<motion.div variants={child} />
</motion.div>