Framer-motion
Framer Motion Animations
Framer Motion Animations Core Concepts import { motion, AnimatePresence, useAnimation, useInView, useScroll, useTransform } from 'framer-motion'; // Basic anima…
Framer Motion Animations
Core Concepts
import { motion, AnimatePresence, useAnimation, useInView, useScroll, useTransform } from 'framer-motion';
// Basic animation — motion.div replaces div
<motion.div
initial={{ opacity: 0, y: 20 }} // starting state
animate={{ opacity: 1, y: 0 }} // target state
exit={{ opacity: 0, y: -20 }} // unmount animation (needs AnimatePresence)
transition={{ duration: 0.3, ease: 'easeOut' }}
/>
// Hover & tap
<motion.button
whileHover={{ scale: 1.05, boxShadow: '0 4px 20px rgba(0,0,0,0.1)' }}
whileTap={{ scale: 0.95 }}
transition={{ type: 'spring', stiffness: 400, damping: 17 }}
/>
// Variants — define states by name
const cardVariants = {
hidden: { opacity: 0, y: 30 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: 'easeOut' },
},
hover: { scale: 1.02 },
};
<motion.div
variants={cardVariants}
initial="hidden"
animate="visible"
whileHover="hover"
/>
// Stagger children
const containerVariants = {
hidden: {},
visible: {
transition: {
staggerChildren: 0.1, // delay between each child
delayChildren: 0.2,
},
},
};
const itemVariants = {
hidden: { opacity: 0, x: -20 },
visible: { opacity: 1, x: 0 },
};
<motion.ul variants={containerVariants} initial="hidden" animate="visible">
{items.map(item => (
<motion.li key={item.id} variants={itemVariants}>
{item.name}
</motion.li>
))}
</motion.ul>AnimatePresence & Page Transitions
// AnimatePresence — enables exit animations
function Modal({ isOpen, onClose, children }) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div // backdrop
key="backdrop"
className="fixed inset-0 bg-black/50"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div // modal
key="modal"
className="fixed inset-x-0 bottom-0 rounded-t-2xl bg-white p-6"
initial={{ y: '100%' }}
animate={{ y: 0 }}
exit={{ y: '100%' }}
transition={{ type: 'spring', damping: 30, stiffness: 300 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
);
}
// Route transitions (Next.js App Router)
// app/template.tsx — re-mounts on each navigation
'use client';
export default function Template({ children }) {
return (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.3 }}
>
{children}
</motion.div>
);
}
// List add/remove with layout animations
<AnimatePresence mode="popLayout">
{items.map(item => (
<motion.div
key={item.id}
layout // animate layout changes
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
>
{item.name}
</motion.div>
))}
</AnimatePresence>Scroll & Gesture Hooks
// Scroll-linked animations
function ParallaxHero() {
const { scrollYProgress } = useScroll();
const y = useTransform(scrollYProgress, [0, 1], [0, -300]);
const opacity = useTransform(scrollYProgress, [0, 0.3], [1, 0]);
return (
<motion.div style={{ y, opacity }}>
<h1>Hero Text</h1>
</motion.div>
);
}
// Animate when element enters viewport
function AnimateOnScroll({ children }) {
const ref = useRef(null);
const isInView = useInView(ref, { once: true, margin: '-100px' });
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 40 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5 }}
>
{children}
</motion.div>
);
}
// Programmatic control
function PulseButton() {
const controls = useAnimation();
const pulse = async () => {
await controls.start({ scale: 1.2, transition: { duration: 0.2 } });
await controls.start({ scale: 1, transition: { duration: 0.2 } });
};
return <motion.button animate={controls} onClick={pulse}>Click me</motion.button>;
}
// Drag
<motion.div
drag="x" // 'x' | 'y' | true (both)
dragConstraints={{ left: -100, right: 100 }}
dragElastic={0.2}
onDragEnd={(e, info) => console.log(info.offset, info.velocity)}
/>