Back to Blog
ReactFramer MotionAnimationUI

React Animations with Framer Motion

Create fluid animations in React. From basic transitions to gestures to complex orchestration patterns.

B
Bootspring Team
Engineering
September 21, 2021
7 min read

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.

Share this article

Help spread the word about Bootspring