React Animation Blog
Vol. 01A beginner's guide to installing, importing, and using motion components to bring your React app to life, with live interactive examples throughout.
8Concepts covered
100%Interactive demos
BeginnerDifficulty
framer-motion v11
Step 1 — Install
1
Install Framer Motion
Make sure you have a React project set up — via create-react-app, Vite, or Next.js. Then in your terminal:
npm install framer-motion
Or if you use Yarn:
yarn add framer-motion
Step 2 — Import
2
Importing Motion
import { motion } from "framer-motion";motionis a special component factory from Framer Motion.- Wrap any HTML element with
motionto animate it —<motion.div>,<motion.h1>,<motion.span>, etc. - It behaves exactly like the original element, but now accepts animation props.
Core Motion Props
| Prop | What it does | Example |
|---|---|---|
initial | Starting state before animation plays | { opacity: 0, y: -40 } |
animate | Target state to animate toward | { opacity: 1, y: 0 } |
transition | Controls how the animation plays | { duration: 0.8 } |
exit | State when element is removed from DOM | { opacity: 0 } |
whileHover | Animates on mouse hover | { scale: 1.05 } |
whileTap | Animates when clicked or tapped | { scale: 0.97 } |
Reading the example:
initial={{ opacity: 0, y: -40 }} means the element starts invisible and 40px above its natural position. animate={{ opacity: 1, y: 0 }} moves it back down and fades it in. The double curly braces are JSX (outer) + JS object (inner).Easing — click to preview
easeIn
Starts slow, ends fast. Good for exits.
easeOut
Starts fast, decelerates. Natural entrances.
easeInOut
Slow → fast → slow. Smooth and polished.
linear
Constant speed. Feels mechanical.
backOut
Overshoots then snaps back. Playful.
anticipate
Pullback before moving. Dramatic.
Transition — tween vs spring
Every Framer Motion animation has a transition prop that controls how it plays. There are two fundamentally different modes — pick the right one and your UI feels alive. Pick the wrong one and it feels scripted.
Tweenduration: 0.8s · ease: easeInOut
Springstiffness: 120 · damping: 14
Notice: the spring overshoots slightly and settles naturally. The tween reaches the end at exactly 0.8s and stops.
Tween keystype: "tween" (default)
transition={{
duration: 0.8,
ease: "easeOut",
delay: 0.2,
}}| Key | What it does | Example |
|---|---|---|
duration | How long the animation takes, in seconds. | 0.8 |
delay | Wait before starting. Stagger multiple elements. | 0.3 |
ease | The easing curve — string or cubic bezier array. | "easeOut" |
Spring keystype: "spring"
transition={{
type: "spring",
stiffness: 120,
damping: 14,
mass: 1,
delay: 0.2,
}}| Key | What it does | Example |
|---|---|---|
type | Set to "spring" to switch from tween to physics. | "spring" |
stiffness | How snappy the spring is. Higher = faster snap. | 120 |
damping | Resistance to bouncing. Lower = more oscillation. | 14 |
mass | Weight of the element. Higher = slower, heavier feel. | 1 |
delay | Works the same as in tweens — wait before starting. | 0.2 |
When to use which: Use
tween for entrance animations, page transitions, and anything decorative where you want exact timing control. Use spring for anything the user can interact with — hover, tap, drag, and focus states. Springs handle mid-animation interruptions naturally: if the user moves their cursor away before a hover animation finishes, a spring settles from wherever it currently is. A tween either completes or jumps — neither feels right.Text entrance animation
direction
ease
duration0.8s
<motion.div
initial={{ opacity: 0, y: -40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: "easeInOut" }}
>
<h1>Hello, world!</h1>
</motion.div>key={0} — increments on each replay
The key prop trick: Framer Motion entrance animations fire once — on mount. To replay them, increment a
key prop on the motion element. React sees a new key, unmounts the old element, mounts a fresh one, and the initial → animate transition fires again from scratch. Without this, the animation runs once and stays frozen. Watch the counter above — it increments every time you hit Replay or change a control.Variants & staggerChildren
import { motion } from "framer-motion";
// Define variants outside the component —
// keeps JSX clean and avoids recreating objects on every render.
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.15, // each child waits 150ms
delayChildren: 0.2, // first child waits 200ms
},
},
};
const item = {
hidden: { opacity: 0, y: 20 },
visible: { opacity: 1, y: 0 },
};
function FeaturesList() {
const features = ["Fast", "Reliable", "Easy to use", "Customizable", "Secure"];
return (
<motion.ul
variants={container}
initial="hidden"
animate="visible"
>
{features.map((feature) => (
// No initial/animate needed on children —
// the parent propagates its state down automatically.
<motion.li key={feature} variants={item}>
{feature}
</motion.li>
))}
</motion.ul>
);
}staggerChildren0.15s
0.05s = fast sweep → 0.6s = very slow cascade
delayChildren0.2s
0s = start immediately → 1s = long initial pause
- ▸Fast
- ▸Reliable
- ▸Easy to use
- ▸Customizable
- ▸Secure
| Concept | What it does |
|---|---|
variants | Named animation states defined as plain objects outside JSX. Keeps markup clean. |
initial="hidden" | Sets the starting state by referencing the variant name — no inline object needed. |
animate="visible" | The target state to animate toward, again by name. |
staggerChildren | Delay between each child's animation start. Creates a cascade effect. |
delayChildren | Extra wait before the very first child begins animating. |
inheritance | Child motion elements inherit the parent's state — no need to repeat initial/animate on each one. |
Why variants? Without variants, animating a list of 10 items means writing
initial, animate, and transition on every single motion.li. Variants let you define those states once and reference them by name. The parent then coordinates timing for all children automatically via staggerChildren — no manual delay calculations needed. Try dragging staggerChildren to 0.4s to see each item land individually, or 0.05s for a near-simultaneous sweep.Drag — drag, dragConstraints & dragElastic
import { motion } from "framer-motion";
function DraggableCard() {
return (
<motion.div
drag
dragConstraints={{ left: -130, right: 130, top: -40, bottom: 40 }}
dragElastic={0.2}
whileDrag={{ scale: 1.08, rotate: 2 }}
>
Drag me!
</motion.div>
);
}dragElastic0.2
0 = hard wall → 1 = fully elastic, no resistance
drag axis
true = both axes "x" = horizontal only "y" = vertical only
dragSnapToOrigin
snaps card back to starting position on release
Drag me!
| Prop | What it does |
|---|---|
drag | Enables dragging. true = both axes, "x" = horizontal, "y" = vertical. |
dragConstraints | Limits drag range in px from the resting position. left/right/top/bottom. |
dragElastic | Overshoot amount past constraints. 0 = hard stop, 1 = fully elastic. |
dragSnapToOrigin | Snaps the element back to its original position when released. |
whileDrag | Animation applied while actively dragging. Auto-reverses on release. |
onDragEnd | Callback fired on release. Receives event + info including final velocity. |
Things to try: Set
dragElastic to 0 for a hard boundary that stops dead, then 1 for a fully elastic feel with no resistance. Toggle dragSnapToOrigin on to make the card always return home — great for UI where dragging is a gesture (like a dismiss action) rather than a repositioning tool. Switch the axis to "x" to lock it to a horizontal slider pattern.AnimatePresence — animating exit
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
function DismissableAlert() {
const [open, setOpen] = useState(false);
return (
<div>
<button onClick={() => setOpen((o) => !o)}>
Toggle alert
</button>
{/*
AnimatePresence must wrap the conditional element.
Without it, React removes it instantly — no exit animation.
*/}
<AnimatePresence mode="sync">
{open && (
<motion.div
key="alert"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.25 }}
>
This is an animated alert.
</motion.div>
)}
</AnimatePresence>
</div>
);
}alert type
switches the alert colour — show/hide to see it re-enter
AnimatePresence mode
sync = simultaneous wait = exit finishes before enter popLayout = instant reflow
exit direction
direction the alert slides when entering and exiting
transition duration0.25s
0.1s = very snappy → 1s = slow and deliberate
| Prop / concept | What it does |
|---|---|
<AnimatePresence> | Wrapper that intercepts unmounts and plays exit animations before removal. |
exit | The animation state to reach before the element is removed from the DOM. |
mode="sync" | Enter and exit animations play at the same time (default). |
mode="wait" | Waits for the exit to fully complete before the enter begins. |
mode="popLayout" | Exiting element is removed from layout flow immediately so others reflow. |
key | AnimatePresence tracks children by key. Changing key = exit old, enter new. |
The key insight: React removes elements from the DOM synchronously — there is no "about to unmount" phase for animations to hook into. This is exactly what
AnimatePresence solves. It intercepts the unmount, plays the exit animation to completion, then lets React remove the element. Try switching mode to "wait" — the alert fully disappears before reappearing, which is useful for page transitions. The key prop on the child is required so AnimatePresence knows which element to track.AnimatePresence — view transitions & step switcher
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
function StepSwitcher() {
const [step, setStep] = useState(1);
return (
<div>
<button onClick={() => setStep((s) => (s === 1 ? 2 : 1))}>
Toggle Step
</button>
{/*
mode="wait" — step1 fully exits before step2 enters.
Each child needs a unique key so AnimatePresence knows
which one is leaving and which one is arriving.
*/}
<AnimatePresence mode="wait">
{step === 1 && (
<motion.div
key="step1"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
>
Step 1
</motion.div>
)}
{step === 2 && (
<motion.div
key="step2"
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.3 }}
>
Step 2
</motion.div>
)}
</AnimatePresence>
</div>
);
}AnimatePresence mode
wait = clean handoff sync = both animate at the same time (try it — feels messier)
transition duration0.3s
slow it down to see the exit and enter phases clearly
directional animation
Next slides up, Prev slides down — spatially reinforces the flow
| Concept | What it does |
|---|---|
key per view | Unique key on each child tells AnimatePresence which is entering and which is leaving. |
mode="wait" | The current view exits fully before the next one enters — a clean sequential handoff. |
mode="sync" | Both animations run at the same time — enter and exit overlap. Often feels busier. |
Mirrored y values | exit: y:-10 mirrors initial: y:10 — content slides out the same direction it came from. |
Directional exit | Flip exit direction based on forward/back navigation to reinforce spatial movement. |
position: absolute | Both views occupy the same space during the transition, so they overlap cleanly. |
The pattern in the wild: This is exactly how page transitions, tab panels, carousels, and onboarding wizards work in production apps. The container has
position: relative and a fixed height — both views use position: absolute so they stack on top of each other during the transition rather than pushing layout around. Toggle directional animation on and click Prev to see the slides reverse direction — a small detail that makes navigation feel spatially grounded.layout — automatic size & position animation
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
function CollapsibleList() {
const [expanded, setExpanded] = useState(null);
const items = [1, 2, 3];
return (
<div>
{items.map((id) => (
/*
layout — when this card's height changes (content appears
or disappears inside), Framer Motion smoothly animates
the resize. No height animation code needed.
*/
<motion.div
key={id}
layout
onClick={() => setExpanded((prev) => (prev === id ? null : id))}
>
<h3>Item {id}</h3>
{/*
AnimatePresence handles the content fade-out.
layout on the parent handles the card resize.
Together they produce a smooth accordion effect.
*/}
<AnimatePresence>
{expanded === id && (
<motion.p
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Expanded content here.
</motion.p>
)}
</AnimatePresence>
</motion.div>
))}
</div>
);
}layout type
true = animate size + position "position" = position only "size" = size only
spring stiffness300
50 = slow and loose → 600 = very snappy resize
spring damping30
5 = bouncy → 60 = no bounce, settles immediately
| Prop / concept | What it does |
|---|---|
layout | Automatically animates any size or position change on this element using FLIP. |
layout="position" | Only animates position changes — ignores size. Good for repositioning items. |
layout="size" | Only animates size changes — ignores position. Good for expanding in-place. |
layoutId | Shared identity across renders. Framer Motion morphs one element into another — used for shared element transitions. |
transition.layout | Controls the speed/easing of layout animations separately from other transitions. |
FLIP technique | Measures DOM before and after change, plays a transform between the two states. No height tweening needed. |
Why layout is special: Before
layout, animating an element's height meant manually tracking pixel values, using max-height hacks, or reaching for a library. Framer Motion's FLIP technique sidesteps all of that — it snapshots the element before and after the DOM change, then plays a smooth transform between the two. Try setting damping to 5 for a bouncy accordion, or stiffness to 600 for an instant-feeling but still-smooth snap. Switch layout type to "position" to see cards reflow without resizing.Keyframes & repeat — looping animations
import { motion } from "framer-motion";
function LiveBadge() {
return (
<motion.span
animate={{
// Array values = keyframe sequence: start → peak → back
scale: [1, 1.15, 1],
boxShadow: [
"0 0 0px rgba(124, 58, 237, 0.3)",
"0 0 12px rgba(124, 58, 237, 0.8)",
"0 0 0px rgba(124, 58, 237, 0.3)",
],
}}
transition={{
duration: 1.2, // one full pulse cycle
repeat: Infinity, // loop forever
repeatType: "reverse", // play forwards then backwards
repeatDelay: 3, // rest 3s between each pulse
ease: "easeInOut",
}}
>
● LIVE
</motion.span>
);
}colour
glow colour is derived from the badge background
repeatType
reverse = fwd then back loop = restart from beginning mirror = reverse with mirrored easing
scale peak1.15
how large the badge grows at the peak of the pulse
duration1.2
0.3s = fast flicker → 3s = slow deep breath
repeatDelay3
0s = continuous pulse → 5s = long rest between pulses
● LIVE
| Prop / concept | What it does |
|---|---|
Keyframe array | Pass an array of values to animate through multiple states: [1, 1.15, 1]. |
repeat: Infinity | Loops the animation forever. Pass a number to loop a fixed number of times. |
repeatType: "loop" | Restarts from the first keyframe each time. Can feel abrupt on non-symmetrical animations. |
repeatType: "reverse" | Plays forwards then backwards, back and forth. Produces a smooth breathing effect. |
repeatType: "mirror" | Like reverse but the easing curve is also mirrored on the way back. |
repeatDelay | Pause in seconds between each cycle. Creates a rest between pulses. |
boxShadow keyframes | Framer Motion can animate box-shadow arrays. All values must share the same shadow structure. |
Keyframes vs A→B: Regular Framer Motion animations go from
initial to animate — one value to another. Keyframe arrays let you chain multiple stops in a single animation: scale: [1, 1.15, 1] is a full pulse in one property. Try setting repeatDelay to 0 for a continuous throb, then 3 for a subtle periodic reminder. Switch repeatType to "loop" to see why"reverse" feels smoother — loop restarts abruptly, reverse eases back naturally. The boxShadow glow is animated the same way — just make sure all array values use identical shadow syntax or Framer Motion can't interpolate between them.