From Zero to Animation Hero with Framer Motion

From Zero to Animation Hero with Framer Motion

Framer Motion
React
Animations
UI/UX
JavaScript
2024-07-28

Framer Motion is a powerful production-ready animation library for React. Traditional CSS animations can be cumbersome to manage for complex interactions, but Framer Motion’s declarative interface and robust features make building dynamic, interactive experiences not just straightforward but also fun.

In this guide, we’ll start from scratch, learning essential concepts like motion components, variants, gestures, layout animations, and even 2D and 3D transformations. By the end, you'll be an “animation hero,” ready to take your UI to the next level and truly show off what's possible in the browser.

First, ensure you have a React project. If you need a quick starting point:

npx create-react-app framer-motion-hero cd framer-motion-hero npm install framer-motion

Or if you’re using Next.js, just add Framer Motion to your dependencies:

npm install framer-motion

That’s it! Framer Motion is now ready for use in your React project.

The simplest way to animate an element is to replace it with a Framer Motion motion component:

import React, { useState } from "react"; import { motion } from "framer-motion"; export default function SimpleFade() { const [keyVal, setKeyVal] = useState(0); const reTriggerAnimation = () => { // incrementing keyVal forces the motion.div to re-mount and replay the animation setKeyVal((prev) => prev + 1); }; return ( <div> <Button variant="outline"variant="outline"onClick={reTriggerAnimation} style={{ marginBottom: "1rem" }}> Re-trigger Fade </Button> <motion.div key={keyVal} style={{ width: 100, height: 100, background: "limegreen" }} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 1 }} /> </div> ); }

Now, when the component mounts, it fades in from opacity 0 to 1 over one second. By adding a “Re-trigger Fade” Button, you can force a remount to see the transition again and again!

Live Demo:

Let’s make a Button that scales up slightly when hovered, and “presses down” on click. You can define these states in a Framer Motion motion.Button.

import React from "react"; import { motion } from "framer-motion"; const InteractiveButton = () => { return ( <motion.Button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.95 }} style={{ padding: "1rem 2rem", fontSize: "1.2rem", background: "purple", color: "white", border: "none", borderRadius: "8px", cursor: "pointer" }} > Click Me </motion.Button> ); }; export default InteractiveButton;

When hovered, the Button grows to scale: 1.1. When tapped, it shrinks to scale: 0.95. It’s subtle, but it adds a clear visual cue for interactivity—with minimal code!

Live Demo:

When dealing with multiple states, Framer Motion’s variants feature is a lifesaver. Variants allow you to define named animation states that can be triggered on parent or child components.

import React, { useState } from "react"; import { motion } from "framer-motion"; const containerVariants = { hidden: { opacity: 0, x: -100 }, visible: { opacity: 1, x: 0, transition: { type: "spring", stiffness: 50, when: "beforeChildren", staggerChildren: 0.2 } } }; const itemVariants = { hidden: { opacity: 0, y: -20 }, visible: { opacity: 1, y: 0 } }; export default function VariantList() { const [showList, setShowList] = useState(false); return ( <div> <Button variant="outline"variant="outline"onClick={() => setShowList(!showList)}> {showList ? "Hide" : "Show"} List </Button> {showList && ( <motion.ul variants={containerVariants} initial="hidden" animate="visible" style={{ listStyle: "none", padding: 0 }} > <motion.li variants={itemVariants} style={{ margin: "10px 0", background: "#333", color: "#fff", padding: "8px", borderRadius: "4px" }} > Item 1 </motion.li> <motion.li variants={itemVariants} style={{ margin: "10px 0", background: "#555", color: "#fff", padding: "8px", borderRadius: "4px" }} > Item 2 </motion.li> <motion.li variants={itemVariants} style={{ margin: "10px 0", background: "#777", color: "#fff", padding: "8px", borderRadius: "4px" }} > Item 3 </motion.li> </motion.ul> )} </div> ); }

Note how containerVariants and itemVariants define the hidden and visible states. When you toggle show/hide, Framer Motion automatically animates each item in sequence.

Live Demo:

Sometimes you need an element to transition through multiple values in a single animation. Framer Motion supports keyframes via an array of values:

import React, { useState } from "react"; import { motion } from "framer-motion"; const KeyframeDemo = () => { const [running, setRunning] = useState(true); return ( <div> <Button onClick={() => setRunning(!running)} style={{ marginBottom: "1rem" }} > {running ? "Stop Animation" : "Start Animation"} </Button> <motion.div style={{ width: 100, height: 100, backgroundColor: "#ff008c", margin: "auto", }} animate={ running ? { x: [0, 100, 100, 0], y: [0, 0, 100, 100] } : { x: 0, y: 0 } } transition={{ duration: 2, ease: "easeInOut", loop: running ? Infinity : 0, repeatDelay: 0.5, }} /> </div> ); }; export default KeyframeDemo;

Here, x and y positions move in a rectangle pattern. A “Start/Stop Animation” Button has been added to let you toggle the animation loop on and off.

Live Demo:

We’ve already seen whileHover and whileTap for simple interactivity. Framer Motion also has robust drag support. Below, we’ve added a “Reset Position” Button to place the box back at its original coordinates any time.

import React, { useState } from "react"; import { motion } from "framer-motion"; const DraggableBox = () => { const [position, setPosition] = useState({ x: 0, y: 0 }); const handleDragEnd = (event, info) => { setPosition({ x: info.point.x, y: info.point.y }); }; const resetPosition = () => { setPosition({ x: 0, y: 0 }); }; return ( <div className="h-60"> <motion.div drag onDragEnd={handleDragEnd} dragConstraints={{ top: -100, left: -100, right: 100, bottom: 100 }} whileHover={{ scale: 1.2 }} whileTap={{ scale: 0.8 }} style={{ width: 100, height: 100, backgroundColor: "coral", cursor: "grab", margin: "0 auto", x: position.x, y: position.y, }} /> </div> ); }; export default DraggableBox;

We constrained dragging to a 200×200 area, so you can move the box around. The box also scales up/down on hover and tap, making it feel more alive.

Live Demo:

Drag Me

Framer Motion can animate layout changes and element presence. This is fantastic when elements are added/removed from the DOM. Wrap them in an AnimatePresence component and provide exit animations:

import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; const FadeList = () => { const [items, setItems] = useState([1, 2, 3]); const removeItem = (item) => { setItems(items.filter(i => i !== item)); }; const resetList = () => { setItems([1, 2, 3]); }; return ( <div> <Button variant="outline"variant="outline"onClick={resetList} style={{ marginBottom: "1rem" }}> Reset List </Button> <AnimatePresence> {items.map(item => ( <motion.div key={item} initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0, scale: 0.5 }} style={{ margin: "8px", background: "#E91E63", color: "white", padding: "8px", borderRadius: "4px" }} onClick={() => removeItem(item)} > Click to Remove: Item {item} </motion.div> ))} </AnimatePresence> </div> ); }; export default FadeList;

As you remove items, they animate out nicely, scaling down and fading away thanks to the exit prop. We’ve also added a “Reset List” Button so you can restore the original items over and over.

Live Demo:

Click to Remove: Item 1
Click to Remove: Item 2
Click to Remove: Item 3

Framer Motion defaults to a spring-based animation. You can fine-tune stiffness, damping, or even choose an ease-based tween. Here we’ve also added a “Re-run Spring” Button to replay the effect.

import React, { useState } from "react"; import { motion } from "framer-motion"; const SpringTuning = () => { const [keyVal, setKeyVal] = useState(0); const rerunAnimation = () => { setKeyVal(prev => prev + 1); }; return ( <div> <Button variant="outline"variant="outline"onClick={rerunAnimation} style={{ marginBottom: "1rem" }}> Re-run Spring </Button> <motion.div key={keyVal} style={{ width: 100, height: 100, background: "#6200ee", margin: "40px auto" }} initial={{ scale: 0 }} animate={{ scale: 1 }} transition={{ type: "spring", stiffness: 200, damping: 10 }} /> </div> ); }; export default SpringTuning;

Tweaking stiffness and damping drastically changes the “feel” of your animation, from bouncy to smooth.

Live Demo:

Let’s combine multiple concepts to build a small interactive gallery. Thumbnails expand into a full view with a fade. We'll use AnimatePresence for toggling the large image.

import React, { useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; const images = [ "https://via.placeholder.com/300/ff7f7f", "https://via.placeholder.com/300/7fff7f", "https://via.placeholder.com/300/7f7fff", ]; export default function Gallery() { const [selectedImg, setSelectedImg] = useState(null); const resetGallery = () => { setSelectedImg(null); }; return ( <div style={{ textAlign: "center" }}> <h2>Interactive Gallery</h2> <div style={{ display: "flex", justifyContent: "center", gap: "1rem" }}> {images.map((url, idx) => ( <motion.img key={idx} src={url} style={{ width: 100, cursor: "pointer" }} whileHover={{ scale: 1.1 }} onClick={() => setSelectedImg(url)} /> ))} </div> <AnimatePresence> {selectedImg && ( <motion.div key="overlay" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} style={{ position: "fixed", top: 0, left: 0, width: "100vw", height: "100vh", backgroundColor: "rgba(0, 0, 0, 0.8)", display: "flex", alignItems: "center", justifyContent: "center" }} onClick={() => setSelectedImg(null)} > <motion.img src={selectedImg} initial={{ scale: 0.8 }} animate={{ scale: 1 }} exit={{ scale: 0.8 }} style={{ maxWidth: "80%", maxHeight: "80%", cursor: "zoom-out" }} /> </motion.div> )} </AnimatePresence> </div> ); }

This snippet demonstrates a ton of features—hover states, toggling a modal-like overlay, AnimatePresence for mounting/unmounting, and scale transitions on images. It’s an excellent illustration of real-world interactivity!

Live Demo:

Interactive Gallery

Want to really show off how cool CSS transformations can be? Let’s add rotation and translation to a single element. With Framer Motion, these transformations are easily combined—and we’ve given you a Button to toggle the animation loop on/off.

import React, { useState } from "react"; import { motion } from "framer-motion"; const TwoDTransform = () => { const [animateIt, setAnimateIt] = useState(true); return ( <div className="h-60"> <Button variant="outline" onClick={() => setAnimateIt(!animateIt)} style={{ marginBottom: "1rem" }} > <span>{animateIt ? "Reset" : "Run"}</span> {animateIt ? <Repeat1 /> : <Play />} </Button> <motion.div initial={{ rotate: 0, x: -150, y: -100 }} animate={ animateIt ? { rotate: 360, x: 150, y: 50 } : { rotate: 0, x: -150, y: -100 } } transition={{ duration: 2, ease: "easeInOut", loop: animateIt ? Infinity : 0, repeatType: "reverse", }} style={{ width: 100, height: 100, backgroundColor: "#3498db", margin: "50px auto", }} /> </div> ); }; export default TwoDTransform;

This will continuously rotate from 0 to 360 degrees while shifting its position on the X and Y axes, then reversing back, giving you a fun spinning shift effect.

Live Demo:

You can push transformations further by introducing 3D perspectives. Let’s create a flip card that rotates on the Y-axis when clicked.

import React, { useState } from "react"; import { motion } from "framer-motion"; export default function FlipCard3D() { const [flipped, setFlipped] = useState(false); const flipVariants = { front: { rotateY: 0 }, back: { rotateY: 180 }, }; const handleReset = () => { setFlipped(false); }; return ( <div style={{ textAlign: "center" }}> <Button variant="outline"variant="outline"onClick={handleReset} style={{ marginBottom: "1rem" }}> Reset Flip </Button> <div style={{ perspective: "1000px", width: 200, height: 300, margin: "auto", position: "relative" }} > <motion.div style={{ width: "100%", height: "100%", position: "absolute", backgroundColor: "#42a5f5", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", backfaceVisibility: "hidden" }} variants={flipVariants} animate={flipped ? "back" : "front"} transition={{ duration: 0.6 }} onClick={() => setFlipped(!flipped)} > <h3 style={{ color: "#fff" }}>FRONT</h3> </motion.div> <motion.div style={{ width: "100%", height: "100%", position: "absolute", backgroundColor: "#ef5350", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", backfaceVisibility: "hidden", transform: "rotateY(180deg)" }} variants={flipVariants} animate={flipped ? "back" : "front"} transition={{ duration: 0.6 }} onClick={() => setFlipped(!flipped)} > <h3 style={{ color: "#fff" }}>BACK</h3> </motion.div> </div> </div> ); }

The 3D flip effect is achieved by wrapping your card in a container with perspective. Each side of the card uses backfaceVisibility to hide the backside and rotates 180 degrees on the Y-axis.

Live Demo:

FRONT

BACK

Parallax is a popular effect where background layers move slower than foreground layers, creating depth. With Framer Motion, you can track the mouse position and shift layers accordingly for a dynamic 2D parallax. We also introduced a “Center” Button to snap everything back to the middle.

import React, { useState } from "react"; import { motion } from "framer-motion"; export default function ParallaxDemo() { // Track mouse position in state const [mouse, setMouse] = useState({ x: 0, y: 0 }); // Track window size in state const [windowSize, setWindowSize] = useState({ width: 0, height: 0 }); // Update windowSize whenever the window is resized useEffect(() => { function updateSize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight }); } // Initialize size on mount updateSize(); // Add a resize listener window.addEventListener("resize", updateSize); return () => { window.removeEventListener("resize", updateSize); }; }, []); // Update mouse coordinates on mouse move const handleMouseMove = useCallback((e: React.MouseEvent<HTMLDivElement>) => { setMouse({ x: e.clientX, y: e.clientY }); }, []); // Button to re-center the parallax layers const handleCenter = useCallback(() => { setMouse({ x: window.innerWidth / 2, y: window.innerHeight / 2, }); }, []); // Common Circle Style const circleStyle: React.CSSProperties = { position: "absolute", width: 200, height: 200, borderRadius: "50%", boxShadow: "0 0 20px rgba(0,0,0,0.2)", }; const MX = 2; // Multiplier for parallax effect return ( <div // A responsive, centered container className="relative w-full sm:w-[70vw] h-[50vh] sm:h-[70vh] mx-auto n8rs-blog-my-8 overflow-hidden bg-[#282c34] n8rs-blog-flex items-center n8rs-blog-justify-center" onMouseMove={handleMouseMove} > {/* "Center" button in the top-left corner */} <Button onClick={handleCenter} className="absolute ton8rs-blog-p-4 left-4 z-10 shadow-lg" > Center </Button> {/* Foreground Layer (moves fastest) */} <motion.div style={{ ...circleStyle, backgroundColor: "tomato", top: "40%", left: "40%", }} animate={{ x: MX * (mouse.x - windowSize.width / 2) * 0.05, y: MX * (mouse.y - windowSize.height / 2) * 0.05, }} /> {/* Middle Layer */} <motion.div style={{ ...circleStyle, backgroundColor: "orange", top: "50%", left: "50%", }} animate={{ x: MX * (mouse.x - windowSize.width / 2) * 0.04, y: MX * (mouse.y - windowSize.height / 2) * 0.04, }} /> {/* Background Layer (moves slowest) */} <motion.div style={{ ...circleStyle, backgroundColor: "gold", top: "60%", left: "60%", }} animate={{ x: MX * (mouse.x - windowSize.width / 2) * 0.03, y: MX * (mouse.y - windowSize.height / 2) * 0.03, }} /> </div> ); }

As you move your mouse around, each layer shifts at different speeds, creating a satisfying 2D parallax effect. The “Center” Button realigns the effect on demand.

Live Demo:

We can push illusions further with perspective transforms that animate the door opening into another scene. Here’s a simplified snippet with a “Close Door” Button to bring it back into view.

import React, { useState } from "react"; import { motion } from "framer-motion"; export default function PaintedDoor() { const [open, setOpen] = useState(false); return ( <div style={{ // Centers content and adds perspective display: "flex", flexDirection: "column", alignItems: "center", margin: "2rem auto", perspective: "1000px", position: "relative", // Keeps the container from stretching oddly on large screens maxWidth: "90vw", }} > {/* "Door" Panel */} <motion.div style={{ // The clamp() ensures a nice range: never smaller than 180px or larger than 320px width: "clamp(180px, 20vw, 320px)", // Maintains a 2:3 ratio height: "clamp(270px, 30vw, 480px)", // color like a door backgroundColor: "#795548", borderRadius: 8, transformOrigin: "left center", cursor: "pointer", // A subtle 3D hover effect boxShadow: "0 4px 6px rgba(0,0,0,0.2)", }} className="n8rs-blog-flex items-center n8rs-blog-justify-center" whileHover={{ scale: 1.03, }} onClick={() => setOpen(!open)} animate={{ rotateY: open ? -90 : 0, }} transition={{ duration: 1 }} > {/* add a door handle and some verticle "panels" */} <div style={{ width: "20%", height: "100%", // adjust color a bit backgroundColor: "#6d4c41", }} /> <div style={{ width: "20%", height: "100%", // adjust color a bit backgroundColor: "#5d4037", }} /> <div style={{ width: "20%", height: "100%", // adjust color a bit backgroundColor: "#6d4c41", }} /> {/* door handle */} <span className=""> <Circle style={{ color: "black", }} /> </span> </motion.div> {/* The “Inside” (Only visible when door is open) */} {open && ( <motion.div style={{ position: "absolute", top: 0, // Matches door width to look seamless left: "clamp(180px, 20vw, 320px)", width: "clamp(180px, 20vw, 320px)", height: "clamp(270px, 30vw, 480px)", backgroundColor: "transparent", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", color: "#fff", }} > <motion.h2 // fade in to allow the door to open first ...slow it down a bit initial={{ scale: 0, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ duration: 2 }} style={{ fontSize: "1.2rem", textAlign: "center" }} > Welcome! </motion.h2> </motion.div> )} </div> ); }

Clicking the “door” rotates it on the Y-axis, revealing a hidden panel behind it. This is a fun way to create illusions of depth, combining 2D and 3D transforms.

Live Demo:

  • One Animation Code Base: Keep your animation definitions co-located with the element or component. Don’t scatter your transitions in separate files.
  • Reduce Reflows: If animating layout changes results in performance bottlenecks, consider using transform for movement instead of top/left/width/height changes.
  • Variants Over Duplicate Props: If multiple children share similar animations, define variantsto keep code DRY.
  • Exit Animations: Wrap your dynamically removed elements in AnimatePresence to ensure they animate off the screen smoothly.
  • 3D Performance: When doing 3D transforms, avoid excessive nesting of elements with heavy images or backgrounds. Keep VR-like illusions in check for performance.
  • Watch Out for SSR: If using Next.js, remember that certain animations only make sense client-side. For non-essential animations, guard them with a check for client rendering.

Building stunning animations in React has never been easier. Framer Motion’s declarative API, advanced features like layout/exit animations, robust gestures, and variant system empower you to deliver imaginative, high-performance UIs with minimal code. We covered everything from basic fade-ins to a 3D flipping card, 2D parallax illusions, and rotating “painted door” illusions. With these techniques (and your newfound knowledge), you’re well on your way to becoming an “animation hero.”

Don’t hesitate to experiment—Framer Motion is designed to support your creativity while keeping code maintainable. The browser is capable of truly spectacular feats when it comes to 2D and 3D transformations, so dream big and animate away.

Go forth and bring your user interfaces to life!
– Nate