Introduction
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.
Step 1: Project Setup
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.
Step 2: Basic Motion Component
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:
Step 3: Interactive Button Animation
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:
Step 4: Using Variants for Complex Animations
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:
Step 5: Keyframes Animations
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:
Step 6: Gesture Animations (Drag, Hover, Tap)
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
Step 7: Layout Animations & AnimatePresence
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:
Step 8: Spring and Transition Customization
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:
Step 9: Example – Interactive Gallery with Thumbnails
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



Step 10: 2D Rotations and Moves
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:
Step 11: 3D Flip Card with Perspective
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:
Step 12: Parallax Scenes
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:
Bonus: Painted Door / Portal Effect (2.5D)
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:
Best Practices & Common Pitfalls
- 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
variants
to 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.
Conclusion
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