framer-motion
SKILL.md
Framer Motion
Basic Animation
import { motion } from 'framer-motion'
function AnimatedBox() {
return (
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5 }}
>
Hello World
</motion.div>
)
}
Animate Properties
<motion.div
animate={{
x: 100,
y: 50,
scale: 1.2,
rotate: 180,
opacity: 0.5,
backgroundColor: '#ff0000',
borderRadius: '50%',
}}
transition={{
duration: 0.5,
ease: 'easeInOut',
delay: 0.2,
}}
/>
Transition Types
// Spring (default)
transition={{ type: 'spring', stiffness: 100, damping: 10 }}
// Tween
transition={{ type: 'tween', duration: 0.5, ease: 'easeInOut' }}
// Inertia
transition={{ type: 'inertia', velocity: 50 }}
// Ease presets
ease: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'circIn' | 'circOut' | 'backIn' | 'backOut' | 'anticipate'
Variants
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
}
const itemVariants = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
}
function List({ items }) {
return (
<motion.ul
variants={containerVariants}
initial="hidden"
animate="visible"
>
{items.map((item) => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>
)
}
Hover, Tap, Focus
<motion.button
whileHover={{ scale: 1.05, backgroundColor: '#3b82f6' }}
whileTap={{ scale: 0.95 }}
whileFocus={{ boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.5)' }}
>
Click me
</motion.button>
Drag
<motion.div
drag // Enable both axes
drag="x" // Horizontal only
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2} // Elasticity outside constraints
dragMomentum={true} // Continue after release
onDragStart={(e, info) => console.log(info.point)}
onDrag={(e, info) => console.log(info.delta)}
onDragEnd={(e, info) => console.log(info.velocity)}
/>
Scroll Animations
import { motion, useScroll, useTransform } from 'framer-motion'
function ParallaxSection() {
const { scrollYProgress } = useScroll()
const y = useTransform(scrollYProgress, [0, 1], [0, -200])
const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [1, 0.5, 0])
return (
<motion.div style={{ y, opacity }}>
Parallax Content
</motion.div>
)
}
// Scroll-triggered animation
function ScrollTriggered() {
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: '-100px' }}
transition={{ duration: 0.6 }}
>
Appears on scroll
</motion.div>
)
}
Layout Animations
function LayoutExample() {
const [isExpanded, setIsExpanded] = useState(false)
return (
<motion.div
layout
onClick={() => setIsExpanded(!isExpanded)}
style={{
width: isExpanded ? 300 : 100,
height: isExpanded ? 200 : 100,
}}
transition={{ layout: { duration: 0.3 } }}
/>
)
}
// Shared layout
import { LayoutGroup } from 'framer-motion'
function Tabs() {
const [selected, setSelected] = useState(0)
return (
<LayoutGroup>
{tabs.map((tab, i) => (
<button key={tab} onClick={() => setSelected(i)}>
{tab}
{selected === i && (
<motion.div layoutId="underline" className="underline" />
)}
</button>
))}
</LayoutGroup>
)
}
AnimatePresence (Exit Animations)
import { AnimatePresence, motion } from 'framer-motion'
function Modal({ isOpen, onClose }) {
return (
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
transition={{ duration: 0.2 }}
>
<ModalContent onClose={onClose} />
</motion.div>
)}
</AnimatePresence>
)
}
useAnimate Hook
import { useAnimate } from 'framer-motion'
function SequenceAnimation() {
const [scope, animate] = useAnimate()
const handleClick = async () => {
await animate(scope.current, { scale: 1.2 })
await animate(scope.current, { rotate: 180 })
await animate(scope.current, { scale: 1, rotate: 0 })
}
return (
<motion.div ref={scope} onClick={handleClick}>
Click for sequence
</motion.div>
)
}
Motion Values
import { motion, useMotionValue, useTransform } from 'framer-motion'
function MotionValueExample() {
const x = useMotionValue(0)
const opacity = useTransform(x, [-200, 0, 200], [0, 1, 0])
const background = useTransform(
x,
[-200, 0, 200],
['#ff0000', '#00ff00', '#0000ff']
)
return (
<motion.div
drag="x"
style={{ x, opacity, background }}
/>
)
}
Stagger Children
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: {
staggerChildren: 0.1,
delayChildren: 0.3,
},
},
}
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} variants={item} />
))}
</motion.ul>
Loading Skeleton
function Skeleton() {
return (
<motion.div
animate={{ opacity: [0.5, 1, 0.5] }}
transition={{ duration: 1.5, repeat: Infinity }}
className="bg-gray-200 rounded h-4"
/>
)
}
Number Counter
import { motion, useSpring, useTransform } from 'framer-motion'
function Counter({ value }) {
const spring = useSpring(0, { stiffness: 100, damping: 30 })
const display = useTransform(spring, (v) => Math.round(v))
useEffect(() => {
spring.set(value)
}, [value, spring])
return <motion.span>{display}</motion.span>
}