npx skills add ...
npx skills add freshtechbro/claudedesignskills --skill motion-framer
npx skills add freshtechbro/claudedesignskills --skill motion-framer
Modern animation library for React and JavaScript. Create smooth, production-ready animations with motion components, variants, gestures (hover/tap/drag), layout animations, AnimatePresence exit animations, spring physics, and scroll-based effects. Use when building interactive UI components, micro-interactions, page transitions, or complex animation sequences.
Motion (formerly Framer Motion) is a production-ready animation library for React and JavaScript that enables declarative, performant animations with minimal code. It provides motion components that wrap HTML elements with animation superpowers, supports gesture recognition (hover, tap, drag, focus), and includes advanced features like layout animations, exit animations, and spring physics.
When to use this skill:
Technology:
Convert any HTML/SVG element into an animatable component by prefixing with motion.:
Every motion component accepts animation props like animate, initial, transition, and gesture props like whileHover, whileTap, etc.
The animate prop defines the target animation state. When values change, Motion automatically animates to them:
Set the initial state before animation using the initial prop:
Set initial={false} to disable initial animations on mount.
Control how animations move between states using the transition prop:
Transition types:
"tween" (default) - Duration-based with easing"spring" - Physics-based spring animation"inertia" - Decelerating animation (used in drag)Organize animation states using named variants for cleaner code and propagation to children:
Variant propagation - Children automatically inherit parent variant states:
Animate on hover using whileHover prop:
Hover with nested elements:
Animate on tap/press using whileTap prop:
Make elements draggable with the drag prop:
Drag events:
Animate components when they're removed from the DOM using AnimatePresence:
Key requirements:
<AnimatePresence>key propexit prop to define exit animationList items with exit animations:
Staggered exit animations:
Automatically animate layout changes (position, size) with the layout prop:
Grid layout animation:
Shared layout animations (layoutId):
Connect two different elements for smooth transitions using layoutId:
Animate elements when they enter the viewport using whileInView:
Viewport options:
once: true - Animation triggers only onceamount: 0.5 - Percentage of element visible (0-1) or "some" | "all"margin: "-100px" - Offset viewport boundariesStaggered scroll animations:
Use spring physics for natural, bouncy animations:
Spring presets:
stiffness: 100, damping: 20stiffness: 200, damping: 10stiffness: 400, damping: 30stiffness: 50, damping: 20Motion provides declarative gesture handlers:
Event info objects contain:
point: { x, y } - Page coordinatesoffset: { x, y } - Offset from drag startvelocity: { x, y } - Drag velocityManually control animations with the useAnimate hook:
Animation controls:
Create spring-animated motion values:
Detect when an element is in viewport:
Combine Motion for React state-based animations and GSAP for complex timelines:
Animate 3D objects using Motion values:
Animate form validation states:
Transform properties (x, y, scale, rotate) are hardware-accelerated:
Motion supports individual transform properties for cleaner code:
Respect user preferences for reduced motion:
Layout animations can be expensive. Optimize with:
layoutId creates shared layout animations but tracks elements globally. Use only when needed.
Problem: Exit animations don't work
Problem: AnimatePresence can't track elements
Problem: Janky animations, poor performance
Problem: Performance issues with many layout-animated elements
Problem: Duplicated animation code, no child orchestration
Problem: Transitions don't apply to specific gestures
This skill includes:
references/
api_reference.md - Complete Motion API referencevariants_patterns.md - Variant patterns and orchestrationgesture_guide.md - Comprehensive gesture handling guidescripts/
animation_generator.py - Generate Motion component boilerplatevariant_builder.py - Interactive variant configuration toolassets/
starter_motion/ - Complete Motion + Vite starter templateexamples/ - Real-world Motion component patterns<motion.div
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
/>// Duration-based
<motion.div
animate={{ x: 100 }}
transition={{ duration: 0.5, ease: "easeInOut" }}
/>
// Spring physics
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
/>
// Different transitions for different properties
<motion.div
animate={{ x: 100, opacity: 1 }}
transition={{
x: { type: "spring", stiffness: 300 },
opacity: { duration: 0.2 }
}}
/>const variants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
exit: { opacity: 0, scale: 0.9 }
}
<motion.div
variants={variants}
initial="hidden"
animate="visible"
exit="exit"
/>const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1 // Stagger child animations
}
}
}
const itemVariants = {
hidden: { x: -20, opacity: 0 },
visible: { x: 0, opacity: 1 }
}
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>// Simple hover effect
<motion.button
whileHover={{ scale: 1.1 }}
transition={{ duration: 0.2 }}
>
Hover me
</motion.button>
// Multiple properties
<motion.div
whileHover={{
scale: 1.05,
backgroundColor: "#f0f0f0",
boxShadow: "0px 10px 30px rgba(0, 0, 0, 0.2)"
}}
>
Hover card
</motion.div>
// With custom transition
<motion.button
whileHover={{
scale: 1.2,
transition: { duration: 0.1 } // Transition for gesture start
}}
transition={{ duration: 0.5 }} // Transition for gesture end
>
Button
</motion.button><motion.div whileHover="hover" variants={cardVariants}>
<motion.h3 variants={titleVariants}>Title</motion.h3>
<motion.img variants={imageVariants} />
</motion.div>// Scale down on tap
<motion.button
whileTap={{ scale: 0.9 }}
>
Click me
</motion.button>
// Combined hover + tap
<motion.button
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.95, rotate: 3 }}
>
Interactive button
</motion.button>
// With variants
const buttonVariants = {
rest: { scale: 1 },
hover: { scale: 1.1 },
pressed: { scale: 0.95 }
}
<motion.button
variants={buttonVariants}
initial="rest"
whileHover="hover"
whileTap="pressed"
>
Button
</motion.button>// Basic dragging (both axes)
<motion.div drag />
// Constrain to axis
<motion.div drag="x" /> // Only horizontal
<motion.div drag="y" /> // Only vertical
// Drag constraints
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
/>
// Drag with parent constraints
<motion.div ref={constraintsRef}>
<motion.div drag dragConstraints={constraintsRef} />
</motion.div>
// Visual feedback while dragging
<motion.div
drag
whileDrag={{
scale: 1.1,
boxShadow: "0px 10px 20px rgba(0,0,0,0.2)",
cursor: "grabbing"
}}
dragElastic={0.1} // Elasticity when dragging outside constraints
dragTransition={{ bounceStiffness: 600, bounceDamping: 20 }}
/><motion.div
drag
onDragStart={(event, info) => console.log(info.point)}
onDrag={(event, info) => console.log(info.offset)}
onDragEnd={(event, info) => console.log(info.velocity)}
/>import { AnimatePresence } from "framer-motion"
// Basic exit animation
<AnimatePresence>
{isVisible && (
<motion.div
key="modal"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
)}
</AnimatePresence><AnimatePresence>
{items.map(item => (
<motion.li
key={item.id}
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 50 }}
layout // Smooth layout shifts
>
{item.name}
</motion.li>
))}
</AnimatePresence>const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
when: "beforeChildren",
staggerChildren: 0.1
}
},
exit: {
opacity: 0,
transition: {
when: "afterChildren",
staggerChildren: 0.05,
staggerDirection: -1 // Reverse order
}
}
}
<AnimatePresence>
{show && (
<motion.div variants={containerVariants} initial="hidden" animate="visible" exit="exit">
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
<motion.div variants={itemVariants} />
</motion.div>
)}
</AnimatePresence>// Animate all layout changes
<motion.div layout />
// Animate only position changes
<motion.div layout="position" />
// Animate only size changes
<motion.div layout="size" />const [columns, setColumns] = useState(3)
<motion.div className="grid">
{items.map(item => (
<motion.div
key={item.id}
layout
transition={{ layout: { duration: 0.3, ease: "easeInOut" } }}
/>
))}
</motion.div>// Tab indicator example
<nav>
{tabs.map(tab => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="underline"
style={{ position: 'absolute', bottom: 0, left: 0, right: 0, height: 2 }}
/>
)}
</button>
))}
</nav>
// Modal opening from thumbnail
<motion.img
src={thumbnail}
layoutId="product-image"
onClick={() => setExpanded(true)}
/>
<AnimatePresence>
{expanded && (
<motion.div layoutId="product-image">
<img src={fullsize} />
</motion.div>
)}
</AnimatePresence><motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.8 }} // once: trigger once, amount: 80% visible
transition={{ duration: 0.5 }}
>
Animates when scrolled into view
</motion.div><motion.ul
initial="hidden"
whileInView="visible"
viewport={{ once: true, amount: 0.3 }}
variants={{
visible: {
opacity: 1,
transition: { staggerChildren: 0.1 }
},
hidden: { opacity: 0 }
}}
>
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
<motion.li variants={itemVariants} />
</motion.ul>// Basic spring
<motion.div
animate={{ scale: 1.2 }}
transition={{ type: "spring" }}
/>
// Customize spring physics
<motion.div
animate={{ x: 100 }}
transition={{
type: "spring",
stiffness: 300, // Higher = faster, snappier (default: 100)
damping: 20, // Higher = less bouncy (default: 10)
mass: 1, // Higher = more inertia (default: 1)
}}
/>
// Visual duration (easier spring control)
<motion.div
animate={{ rotate: 90 }}
transition={{
type: "spring",
visualDuration: 0.5, // Perceived duration
bounce: 0.25 // Bounciness (0-1, default: 0.25)
}}
/><motion.div
whileHover={{ scale: 1.1 }} // Pointer hovers over element
whileTap={{ scale: 0.9 }} // Primary pointer presses element
whileFocus={{ outline: "2px" }} // Element gains focus
whileDrag={{ scale: 1.1 }} // Element is being dragged
whileInView={{ opacity: 1 }} // Element is in viewport
/><motion.div
onHoverStart={(event, info) => {}}
onHoverEnd={(event, info) => {}}
onTap={(event, info) => {}}
onTapStart={(event, info) => {}}
onTapCancel={(event, info) => {}}
onDragStart={(event, info) => {}}
onDrag={(event, info) => {}}
onDragEnd={(event, info) => {}}
onViewportEnter={(entry) => {}}
onViewportLeave={(entry) => {}}
/>import { useAnimate } from "framer-motion"
function Component() {
const [scope, animate] = useAnimate()
useEffect(() => {
// Animate multiple elements
animate([
[scope.current, { opacity: 1 }],
["li", { x: 0, opacity: 1 }, { delay: stagger(0.1) }],
[".button", { scale: 1.2 }]
])
}, [])
return (
<div ref={scope}>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
<button className="button">Click</button>
</div>
)
}const controls = animate(element, { x: 100 })
controls.play()
controls.pause()
controls.stop()
controls.speed = 0.5
controls.time = 0 // Seek to startimport { useSpring } from "framer-motion"
function Component() {
const x = useSpring(0, { stiffness: 300, damping: 20 })
return (
<motion.div style={{ x }}>
<button onClick={() => x.set(100)}>Move</button>
</motion.div>
)
}import { useInView } from "framer-motion"
function Component() {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, amount: 0.5 })
return (
<div ref={ref}>
{isInView ? "In view!" : "Not in view"}
</div>
)
}import { motion } from "framer-motion"
import gsap from "gsap"
function Component() {
const boxRef = useRef()
const handleClick = () => {
// Use GSAP for complex timeline
const tl = gsap.timeline()
tl.to(boxRef.current, { rotation: 360, duration: 1 })
.to(boxRef.current, { scale: 1.5, duration: 0.5 })
}
return (
// Use Motion for hover/tap/layout animations
<motion.div
ref={boxRef}
whileHover={{ scale: 1.1 }}
onClick={handleClick}
/>
)
}import { motion } from "framer-motion"
import { useFrame } from "@react-three/fiber"
function Box() {
const x = useMotionValue(0)
useFrame(() => {
// Sync Motion value with Three.js position
meshRef.current.position.x = x.get()
})
return (
<>
<mesh ref={meshRef}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<motion.div
style={{ x }}
drag="x"
dragConstraints={{ left: -5, right: 5 }}
/>
</>
)
}import { motion, AnimatePresence } from "framer-motion"
function FormField({ error }) {
return (
<div>
<motion.input
animate={{
borderColor: error ? "#ff0000" : "#cccccc",
x: error ? [0, -10, 10, -10, 10, 0] : 0 // Shake animation
}}
transition={{ duration: 0.4 }}
/>
<AnimatePresence>
{error && (
<motion.p
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
style={{ color: "#ff0000" }}
>
{error}
</motion.p>
)}
</AnimatePresence>
</div>
)
}// ✅ Good - Hardware accelerated
<motion.div animate={{ x: 100, scale: 1.2 }} />
// ❌ Avoid - Triggers layout/paint
<motion.div animate={{ left: 100, width: 200 }} />// Individual properties (Motion feature)
<motion.div style={{ x: 100, rotate: 45, scale: 1.2 }} />
// Traditional (also supported)
<motion.div style={{ transform: "translateX(100px) rotate(45deg) scale(1.2)" }} />import { useReducedMotion } from "framer-motion"
function Component() {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
animate={{ x: 100 }}
transition={shouldReduceMotion ? { duration: 0 } : { duration: 0.5 }}
/>
)
}// Specify what to animate
<motion.div layout="position" /> // Only position, not size
// Optimize transition
<motion.div
layout
transition={{
layout: { duration: 0.3, ease: "easeOut" }
}}
/>// ❌ Wrong - No AnimatePresence
{show && <motion.div exit={{ opacity: 0 }} />}// ✅ Correct - Wrapped in AnimatePresence
<AnimatePresence>
{show && <motion.div exit={{ opacity: 0 }} />}
</AnimatePresence>// ❌ Wrong - No key
<AnimatePresence>
{items.map(item => <motion.div exit={{ opacity: 0 }} />)}
</AnimatePresence>// ✅ Correct - Unique keys
<AnimatePresence>
{items.map(item => (
<motion.div key={item.id} exit={{ opacity: 0 }} />
))}
</AnimatePresence>// ❌ Avoid - Not hardware accelerated
<motion.div animate={{ top: 100, left: 50, width: 200 }} />// ✅ Better - Use transforms
<motion.div animate={{ x: 50, y: 100, scaleX: 2 }} />// ❌ Too many layout animations
{items.map(item => <motion.div layout>{item}</motion.div>)}// ✅ Use layout only where needed, optimize others
{items.map(item => (
<motion.div
key={item.id}
animate={{ opacity: 1 }} // Cheaper animation
exit={{ opacity: 0 }}
/>
))}// ❌ Repetitive
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} />// ✅ Use variants
const variants = {
hidden: { opacity: 0 },
visible: { opacity: 1 }
}
<motion.div variants={variants} initial="hidden" animate="visible" />
<motion.div variants={variants} initial="hidden" animate="visible" />// ❌ Wrong - General transition won't apply to whileHover
<motion.div
whileHover={{ scale: 1.2 }}
transition={{ duration: 1 }} // This applies to animate prop, not whileHover
/>// ✅ Correct - Transition in whileHover or separate gesture transition
<motion.div
whileHover={{
scale: 1.2,
transition: { duration: 0.2 } // Applies to hover start
}}
transition={{ duration: 0.5 }} // Applies to hover end
/>