Framer Motion makes React animations declarative and powerful. Here's how to use it effectively.
Basic Animations#
1import { motion } from 'framer-motion';
2
3// Simple animation
4function FadeIn() {
5 return (
6 <motion.div
7 initial={{ opacity: 0 }}
8 animate={{ opacity: 1 }}
9 transition={{ duration: 0.5 }}
10 >
11 Hello World
12 </motion.div>
13 );
14}
15
16// Multiple properties
17function SlideIn() {
18 return (
19 <motion.div
20 initial={{ opacity: 0, x: -100 }}
21 animate={{ opacity: 1, x: 0 }}
22 transition={{ duration: 0.5, ease: 'easeOut' }}
23 >
24 Sliding content
25 </motion.div>
26 );
27}
28
29// Scale animation
30function ScaleIn() {
31 return (
32 <motion.div
33 initial={{ scale: 0 }}
34 animate={{ scale: 1 }}
35 transition={{
36 type: 'spring',
37 stiffness: 260,
38 damping: 20,
39 }}
40 >
41 Bouncy!
42 </motion.div>
43 );
44}Variants#
1// Define animation states
2const containerVariants = {
3 hidden: { opacity: 0 },
4 visible: {
5 opacity: 1,
6 transition: {
7 staggerChildren: 0.1,
8 },
9 },
10};
11
12const itemVariants = {
13 hidden: { opacity: 0, y: 20 },
14 visible: {
15 opacity: 1,
16 y: 0,
17 transition: {
18 duration: 0.5,
19 },
20 },
21};
22
23function List({ items }: { items: string[] }) {
24 return (
25 <motion.ul
26 variants={containerVariants}
27 initial="hidden"
28 animate="visible"
29 >
30 {items.map((item) => (
31 <motion.li key={item} variants={itemVariants}>
32 {item}
33 </motion.li>
34 ))}
35 </motion.ul>
36 );
37}
38
39// Dynamic variants
40const buttonVariants = {
41 idle: { scale: 1 },
42 hover: { scale: 1.05 },
43 tap: { scale: 0.95 },
44 disabled: { opacity: 0.5 },
45};
46
47function Button({ disabled }: { disabled?: boolean }) {
48 return (
49 <motion.button
50 variants={buttonVariants}
51 initial="idle"
52 whileHover={disabled ? undefined : 'hover'}
53 whileTap={disabled ? undefined : 'tap'}
54 animate={disabled ? 'disabled' : 'idle'}
55 >
56 Click me
57 </motion.button>
58 );
59}Gestures#
1// Hover and tap
2function InteractiveCard() {
3 return (
4 <motion.div
5 whileHover={{ scale: 1.02, boxShadow: '0 10px 30px rgba(0,0,0,0.2)' }}
6 whileTap={{ scale: 0.98 }}
7 transition={{ type: 'spring', stiffness: 400 }}
8 className="card"
9 >
10 Hover or tap me
11 </motion.div>
12 );
13}
14
15// Drag
16function DraggableBox() {
17 return (
18 <motion.div
19 drag
20 dragConstraints={{ left: -100, right: 100, top: -100, bottom: 100 }}
21 dragElastic={0.2}
22 whileDrag={{ scale: 1.1 }}
23 className="box"
24 >
25 Drag me
26 </motion.div>
27 );
28}
29
30// Drag with snap back
31function SnapBack() {
32 return (
33 <motion.div
34 drag="x"
35 dragConstraints={{ left: 0, right: 0 }}
36 dragElastic={0.5}
37 onDragEnd={(_, info) => {
38 if (Math.abs(info.offset.x) > 100) {
39 // Handle swipe
40 console.log(info.offset.x > 0 ? 'Swiped right' : 'Swiped left');
41 }
42 }}
43 >
44 Swipe me
45 </motion.div>
46 );
47}
48
49// Pan gesture
50function PanGesture() {
51 const [position, setPosition] = useState({ x: 0, y: 0 });
52
53 return (
54 <motion.div
55 onPan={(_, info) => {
56 setPosition({
57 x: position.x + info.delta.x,
58 y: position.y + info.delta.y,
59 });
60 }}
61 animate={position}
62 >
63 Pan me
64 </motion.div>
65 );
66}AnimatePresence#
1import { motion, AnimatePresence } from 'framer-motion';
2
3// Exit animations
4function Modal({ isOpen, onClose, children }: ModalProps) {
5 return (
6 <AnimatePresence>
7 {isOpen && (
8 <>
9 <motion.div
10 className="overlay"
11 initial={{ opacity: 0 }}
12 animate={{ opacity: 1 }}
13 exit={{ opacity: 0 }}
14 onClick={onClose}
15 />
16 <motion.div
17 className="modal"
18 initial={{ opacity: 0, scale: 0.9, y: 20 }}
19 animate={{ opacity: 1, scale: 1, y: 0 }}
20 exit={{ opacity: 0, scale: 0.9, y: 20 }}
21 transition={{ type: 'spring', damping: 25 }}
22 >
23 {children}
24 </motion.div>
25 </>
26 )}
27 </AnimatePresence>
28 );
29}
30
31// List with exit animations
32function AnimatedList({ items }: { items: Item[] }) {
33 return (
34 <ul>
35 <AnimatePresence>
36 {items.map((item) => (
37 <motion.li
38 key={item.id}
39 initial={{ opacity: 0, height: 0 }}
40 animate={{ opacity: 1, height: 'auto' }}
41 exit={{ opacity: 0, height: 0 }}
42 transition={{ duration: 0.2 }}
43 >
44 {item.text}
45 </motion.li>
46 ))}
47 </AnimatePresence>
48 </ul>
49 );
50}
51
52// Mode: wait for exit before enter
53function PageTransition({ page }: { page: string }) {
54 return (
55 <AnimatePresence mode="wait">
56 <motion.div
57 key={page}
58 initial={{ opacity: 0, x: 20 }}
59 animate={{ opacity: 1, x: 0 }}
60 exit={{ opacity: 0, x: -20 }}
61 transition={{ duration: 0.3 }}
62 >
63 {page === 'home' && <HomePage />}
64 {page === 'about' && <AboutPage />}
65 </motion.div>
66 </AnimatePresence>
67 );
68}Layout Animations#
1// Automatic layout animation
2function ExpandingCard() {
3 const [isExpanded, setIsExpanded] = useState(false);
4
5 return (
6 <motion.div
7 layout
8 onClick={() => setIsExpanded(!isExpanded)}
9 style={{
10 width: isExpanded ? 300 : 150,
11 height: isExpanded ? 200 : 100,
12 }}
13 className="card"
14 >
15 <motion.h2 layout>Title</motion.h2>
16 {isExpanded && (
17 <motion.p
18 initial={{ opacity: 0 }}
19 animate={{ opacity: 1 }}
20 >
21 Expanded content here
22 </motion.p>
23 )}
24 </motion.div>
25 );
26}
27
28// Shared layout animations
29function SharedLayoutExample() {
30 const [selectedId, setSelectedId] = useState<string | null>(null);
31
32 return (
33 <>
34 <div className="grid">
35 {items.map((item) => (
36 <motion.div
37 key={item.id}
38 layoutId={item.id}
39 onClick={() => setSelectedId(item.id)}
40 className="card"
41 >
42 <motion.h2 layoutId={`title-${item.id}`}>
43 {item.title}
44 </motion.h2>
45 </motion.div>
46 ))}
47 </div>
48
49 <AnimatePresence>
50 {selectedId && (
51 <motion.div
52 layoutId={selectedId}
53 className="expanded-card"
54 onClick={() => setSelectedId(null)}
55 >
56 <motion.h2 layoutId={`title-${selectedId}`}>
57 {items.find(i => i.id === selectedId)?.title}
58 </motion.h2>
59 <motion.p
60 initial={{ opacity: 0 }}
61 animate={{ opacity: 1 }}
62 >
63 Full content here...
64 </motion.p>
65 </motion.div>
66 )}
67 </AnimatePresence>
68 </>
69 );
70}
71
72// Reorder list
73import { Reorder } from 'framer-motion';
74
75function ReorderList() {
76 const [items, setItems] = useState([1, 2, 3, 4]);
77
78 return (
79 <Reorder.Group values={items} onReorder={setItems}>
80 {items.map((item) => (
81 <Reorder.Item key={item} value={item}>
82 Item {item}
83 </Reorder.Item>
84 ))}
85 </Reorder.Group>
86 );
87}Scroll Animations#
1import { motion, useScroll, useTransform } from 'framer-motion';
2
3// Scroll progress
4function ScrollProgress() {
5 const { scrollYProgress } = useScroll();
6
7 return (
8 <motion.div
9 className="progress-bar"
10 style={{ scaleX: scrollYProgress }}
11 />
12 );
13}
14
15// Parallax effect
16function ParallaxSection() {
17 const { scrollYProgress } = useScroll();
18 const y = useTransform(scrollYProgress, [0, 1], [0, -200]);
19
20 return (
21 <motion.div style={{ y }}>
22 Parallax content
23 </motion.div>
24 );
25}
26
27// Scroll-triggered animation
28function ScrollTriggered() {
29 const ref = useRef(null);
30 const { scrollYProgress } = useScroll({
31 target: ref,
32 offset: ['start end', 'end start'],
33 });
34
35 const opacity = useTransform(scrollYProgress, [0, 0.5, 1], [0, 1, 0]);
36 const scale = useTransform(scrollYProgress, [0, 0.5, 1], [0.8, 1, 0.8]);
37
38 return (
39 <motion.div
40 ref={ref}
41 style={{ opacity, scale }}
42 >
43 Scroll to reveal
44 </motion.div>
45 );
46}
47
48// useInView
49import { useInView } from 'framer-motion';
50
51function FadeInWhenVisible({ children }: { children: React.ReactNode }) {
52 const ref = useRef(null);
53 const isInView = useInView(ref, { once: true, margin: '-100px' });
54
55 return (
56 <motion.div
57 ref={ref}
58 initial={{ opacity: 0, y: 50 }}
59 animate={isInView ? { opacity: 1, y: 0 } : { opacity: 0, y: 50 }}
60 transition={{ duration: 0.5 }}
61 >
62 {children}
63 </motion.div>
64 );
65}Animation Controls#
1import { motion, useAnimation } from 'framer-motion';
2
3function ControlledAnimation() {
4 const controls = useAnimation();
5
6 async function sequence() {
7 await controls.start({ x: 100, transition: { duration: 0.5 } });
8 await controls.start({ y: 100, transition: { duration: 0.5 } });
9 await controls.start({ x: 0, transition: { duration: 0.5 } });
10 await controls.start({ y: 0, transition: { duration: 0.5 } });
11 }
12
13 return (
14 <>
15 <motion.div animate={controls} className="box" />
16 <button onClick={sequence}>Animate</button>
17 <button onClick={() => controls.stop()}>Stop</button>
18 </>
19 );
20}
21
22// Orchestrated animations
23function OrchestratedAnimation() {
24 const controls = useAnimation();
25
26 useEffect(() => {
27 controls.start((i) => ({
28 opacity: 1,
29 y: 0,
30 transition: { delay: i * 0.1 },
31 }));
32 }, [controls]);
33
34 return (
35 <div>
36 {items.map((item, i) => (
37 <motion.div
38 key={item}
39 custom={i}
40 initial={{ opacity: 0, y: 20 }}
41 animate={controls}
42 >
43 {item}
44 </motion.div>
45 ))}
46 </div>
47 );
48}Performance#
1// Use will-change for better performance
2const optimizedVariants = {
3 hidden: { opacity: 0, y: 20 },
4 visible: {
5 opacity: 1,
6 y: 0,
7 transition: { duration: 0.3 },
8 },
9};
10
11// Reduce motion for accessibility
12import { useReducedMotion } from 'framer-motion';
13
14function AccessibleAnimation() {
15 const shouldReduceMotion = useReducedMotion();
16
17 return (
18 <motion.div
19 initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
20 animate={{ opacity: 1, y: 0 }}
21 transition={{ duration: shouldReduceMotion ? 0 : 0.5 }}
22 >
23 Content
24 </motion.div>
25 );
26}
27
28// Lazy motion for smaller bundles
29import { LazyMotion, domAnimation, m } from 'framer-motion';
30
31function App() {
32 return (
33 <LazyMotion features={domAnimation}>
34 <m.div animate={{ opacity: 1 }}>
35 Lazy loaded animations
36 </m.div>
37 </LazyMotion>
38 );
39}Best Practices#
Performance:
✓ Use transform properties (x, y, scale, rotate)
✓ Avoid animating layout properties
✓ Use will-change sparingly
✓ Lazy load animation features
Accessibility:
✓ Respect prefers-reduced-motion
✓ Don't rely solely on animation for meaning
✓ Keep animations subtle
✓ Allow users to pause animations
Design:
✓ Keep animations short (200-500ms)
✓ Use appropriate easing
✓ Be consistent across the app
✓ Animation should enhance, not distract
Conclusion#
Framer Motion provides powerful, declarative animations for React. Use variants for reusable animation states, AnimatePresence for exit animations, and layout animations for fluid UI changes. Always consider performance and accessibility when adding animations.