Framer Motion, now published as Motion for React, animates any element you swap for a motion component: set initial, animate and transition props. The 25 recipes below run live on this page, each with copyable code: entrances, stagger, drag, layout and shared elements, exit animations, scroll progress, page transitions and reduced motion.
| Package | Import from |
|---|---|
| motion (current name) | motion/react |
| framer-motion (older name, still published) | framer-motion |
Updated
Every demo here is the code beside it, running in your browser. Recipes use the motion package; if your project still uses framer-motion, the components and props are the same and only the import path differs.
Want a no-JavaScript effect instead? Try the CSS animation generator.
| API | What it does | Recipes |
|---|---|---|
| initial / animate / exit | Start state, target state, leaving state | 1, 2, 13 |
| transition | Duration, easing, spring settings, delay, repeat | 5, 24 |
| variants | Named states that children inherit | 3 |
| whileHover / whileTap / whileInView | Animate while a gesture or visibility lasts | 4, 6 |
| drag / dragConstraints | Make an element draggable inside limits | 7, 8 |
| layout / layoutId | Animate size, position and shared elements | 11, 12, 16 |
| AnimatePresence | Keep leaving children mounted for exit | 13 to 17 |
| useScroll / useTransform | Scroll progress and value mapping | 18, 19 |
| useMotionValue / animate() | Values that change without React renders | 20 |
| useAnimate | Imperative sequences scoped to a ref | 23 |
| MotionConfig / useReducedMotion | Respect the reduced-motion setting | 25 |
initial is the first frame, animate is where it ends. Nothing else is needed for a one-off entrance.
import { motion } from "motion/react";
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6 }}
>
Hello
</motion.div>Moving with y uses a CSS transform, so the element never pushes its neighbours while it travels.
import { motion } from "motion/react";
<motion.div
initial={{ opacity: 0, y: 24 }}
animate={{ opacity: 1, y: 0 }}
transition={{ type: "spring", stiffness: 260, damping: 22 }}
>
Card
</motion.div>Variants are named states. The parent passes its state down and staggerChildren delays each child a little more than the last.
import { motion } from "motion/react";
const list = { show: { transition: { staggerChildren: 0.08 } } };
const item = { hidden: { opacity: 0, x: -16 }, show: { opacity: 1, x: 0 } };
<motion.ul variants={list} initial="hidden" animate="show">
{["Plan", "Design", "Build", "Launch"].map((t) => (
<motion.li key={t} variants={item}>{t}</motion.li>
))}
</motion.ul>whileInView starts the animation when the element enters the viewport; once: true stops it replaying on every scroll.
import { motion } from "motion/react";
<motion.section
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, amount: 0.4 }}
>
Section content
</motion.section>An array is a list of keyframes. times spreads them across the duration, from 0 to 1.
import { motion } from "motion/react";
<motion.div
animate={{ scale: [1, 1.25, 1], rotate: [0, 0, 180] }}
transition={{ duration: 1.6, times: [0, 0.5, 1], repeat: Infinity, repeatDelay: 0.6 }}
style={{ width: 48, height: 48, borderRadius: 12, background: "#4f98fb" }}
/>whileHover and whileTap animate to a state while the gesture lasts, then return on their own.
import { motion } from "motion/react";
<motion.button
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 400, damping: 17 }}
>
Press me
</motion.button>dragConstraints can take a ref to a parent element, so the item can never be dragged outside it.
import { motion } from "motion/react";
import { useRef } from "react";
function DragBox() {
const area = useRef(null);
return (
<div ref={area} className="area">
<motion.div drag dragConstraints={area} dragElastic={0.2} className="ball" />
</div>
);
}Drag on one axis, then read the offset and velocity in onDragEnd to decide whether the card goes or snaps back.
import { motion } from "motion/react";
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
onDragEnd={(e, info) => {
if (Math.abs(info.offset.x) > 100 || Math.abs(info.velocity.x) > 500) dismiss();
}}
>
Swipe me
</motion.div>Reorder.Group keeps the array in state and hands you the new order. Each Reorder.Item needs the value it stands for.
import { Reorder } from "motion/react";
import { useState } from "react";
function Todo() {
const [items, setItems] = useState(["Logo", "Menu", "Hero", "Footer"]);
return (
<Reorder.Group axis="y" values={items} onReorder={setItems}>
{items.map((it) => (
<Reorder.Item key={it} value={it}>{it}</Reorder.Item>
))}
</Reorder.Group>
);
}Animating x through a few keyframes gives the familiar shake. Trigger it by changing a key or calling animate from useAnimate.
import { motion } from "motion/react";
<motion.input
key={attempt}
animate={attempt > 0 ? { x: [0, -10, 10, -6, 6, 0] } : {}}
transition={{ duration: 0.4 }}
aria-invalid={attempt > 0}
/>Add layout and change the size or position with ordinary CSS. Motion measures before and after and animates the difference with transforms.
import { motion } from "motion/react";
<motion.div
layout
onClick={() => setOpen(!open)}
style={{ width: open ? 240 : 80, height: open ? 120 : 80 }}
transition={{ type: "spring", stiffness: 300, damping: 30 }}
/>Two elements with the same layoutId are treated as one. When the underline renders under a new tab, it glides there.
import { motion } from "motion/react";
{tabs.map((t) => (
<button key={t} onClick={() => setActive(t)} className="tab">
{t}
{active === t && <motion.span layoutId="underline" className="underline" />}
</button>
))}React removes elements at once. Wrapping them in AnimatePresence keeps a leaving child mounted until its exit animation ends.
import { AnimatePresence, motion } from "motion/react";
<AnimatePresence>
{show && (
<motion.div
key="box"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.8 }}
/>
)}
</AnimatePresence>Backdrop and panel fade together; the panel also rises. Keep the real dialog semantics (role, focus, Escape) in your own code.
<AnimatePresence>
{open && (
<motion.div className="backdrop" onClick={close}
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
<motion.div role="dialog" aria-modal="true" aria-labelledby="m-title"
onClick={(e) => e.stopPropagation()}
initial={{ y: 40, opacity: 0 }} animate={{ y: 0, opacity: 1 }} exit={{ y: 40, opacity: 0 }}>
<h2 id="m-title">Saved</h2>
<button onClick={close}>Close</button>
</motion.div>
</motion.div>
)}
</AnimatePresence>Motion can animate to height: "auto", which plain CSS transitions cannot. overflow hidden stops the text spilling while it opens.
<button aria-expanded={open} aria-controls="faq-1" onClick={() => setOpen(!open)}>
What is included?
</button>
<AnimatePresence initial={false}>
{open && (
<motion.div id="faq-1" key="body"
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
style={{ overflow: "hidden" }}>
<p>Hosting, a free address and the editor.</p>
</motion.div>
)}
</AnimatePresence>layout on each toast lets the rest slide up smoothly when one leaves. popLayout makes the leaving toast stop taking up space at once.
<ul className="toasts">
<AnimatePresence mode="popLayout">
{toasts.map((t) => (
<motion.li key={t.id} layout
initial={{ opacity: 0, x: 40 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 40 }}>
{t.text}
</motion.li>
))}
</AnimatePresence>
</ul>mode="wait" lets the old page finish leaving before the new one enters. The key must change with the route.
<AnimatePresence mode="wait">
<motion.main key={route}
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.2 }}>
{pages[route]}
</motion.main>
</AnimatePresence>useScroll gives scrollYProgress from 0 to 1. Pass container to track a scrolling box instead of the window, and scale the bar with it.
import { motion, useScroll } from "motion/react";
import { useRef } from "react";
function Article() {
const box = useRef(null);
const { scrollYProgress } = useScroll({ container: box });
return (
<div ref={box} style={{ height: 200, overflowY: "auto" }}>
<motion.div style={{ scaleX: scrollYProgress, transformOrigin: "0%" }} className="bar" />
{/* long content */}
</div>
);
}useTransform maps one motion value onto another range, so a layer can move at a different speed from the scroll.
const { scrollYProgress } = useScroll({ container: box });
const y = useTransform(scrollYProgress, [0, 1], [0, -120]);
<motion.img src="/hills.webp" alt="" style={{ y }} />animate() can drive a motion value. useTransform rounds it, and rendering the motion value directly avoids a React render on every frame.
import { animate, motion, useMotionValue, useTransform } from "motion/react";
import { useEffect } from "react";
function Counter({ to }) {
const n = useMotionValue(0);
const rounded = useTransform(n, (v) => Math.round(v).toLocaleString());
useEffect(() => {
const controls = animate(n, to, { duration: 1.2, ease: "easeOut" });
return () => controls.stop();
}, [to]);
return <motion.span>{rounded}</motion.span>;
}pathLength runs from 0 to 1 on any path, circle or line, which is the quickest way to draw a tick or a signature.
<svg viewBox="0 0 50 50" width="80">
<motion.path
d="M10 26 L21 37 L41 14"
fill="none" stroke="#16a34a" strokeWidth="4" strokeLinecap="round"
initial={{ pathLength: 0 }}
animate={{ pathLength: 1 }}
transition={{ duration: 0.8, ease: "easeInOut" }}
/>
</svg>repeat: Infinity with a linear ease gives a steady spin. Pair it with visible or screen-reader text that says what is loading.
<motion.div
role="status"
aria-label="Loading"
animate={{ rotate: 360 }}
transition={{ repeat: Infinity, duration: 0.9, ease: "linear" }}
className="spinner"
/>useAnimate returns a scope ref and an animate function. Awaiting each call runs steps in order, and stagger spreads one step across many elements.
import { stagger, useAnimate } from "motion/react";
function Sequence() {
const [scope, animate] = useAnimate();
async function play() {
await animate("li", { opacity: 0, x: -20 }, { duration: 0 });
await animate("li", { opacity: 1, x: 0 }, { delay: stagger(0.1) });
await animate("button", { scale: [1, 1.1, 1] });
}
return (
<div ref={scope}>
<ul><li>One</li><li>Two</li><li>Three</li></ul>
<button onClick={play}>Play</button>
</div>
);
}Stiffness sets how hard the spring pulls, damping how quickly it settles. Low damping overshoots and wobbles.
<motion.div
animate={{ x: on ? 160 : 0 }}
transition={{ type: "spring", stiffness: 120, damping: 8, mass: 1 }}
/>MotionConfig reducedMotion="user" turns off transform and layout animations for people who ask their system for less motion, while keeping fades.
import { MotionConfig } from "motion/react";
export default function App({ children }) {
return <MotionConfig reducedMotion="user">{children}</MotionConfig>;
}
// Or decide per component:
const reduce = useReducedMotion();
<motion.div animate={{ x: reduce ? 0 : 100, opacity: 1 }} />A template.tsx file remounts on every navigation, unlike layout.tsx, so its initial to animate runs each time you change page. It needs "use client" because it uses motion.
// app/template.tsx
"use client";
import { motion } from "motion/react";
export default function Template({ children }: { children: React.ReactNode }) {
return (
<motion.div initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.25 }}>
{children}
</motion.div>
);
}The m component plus LazyMotion loads only the features you list. domAnimation covers animations, variants, exit and gestures; domMax adds drag and layout.
import { LazyMotion, domAnimation } from "motion/react";
import * as m from "motion/react-m";
export function Shell({ children }) {
return (
<LazyMotion features={domAnimation} strict>
<m.div animate={{ opacity: 1 }} initial={{ opacity: 0 }}>{children}</m.div>
</LazyMotion>
);
}
Plain CSS transitions are enough for hover colours and simple fades. Reach for Motion when an animation depends on React state, needs an exit, animates layout or height auto, follows a gesture, or links to scroll.
For a single keyframe effect with no logic, our CSS animation generator writes the CSS for you and adds no JavaScript at all.
Forgetting the key on AnimatePresence children, which makes React reuse the node and skip the exit.
Wrapping a whole page in layout, which makes every small change animate. Put layout only on the element that moves.
Animating box-shadow or filter on long lists. Both repaint each frame; fade a pseudo-element instead.
Yes. The library was renamed Motion and the React part is imported from motion/react. The framer-motion package is still published with the same API, so older tutorials keep working.
The element must be a direct child of AnimatePresence, have a unique key, and have an exit prop. If the parent of AnimatePresence is removed too, nothing is left to animate.
Animate height from 0 to "auto" on a motion element and set overflow to hidden. Motion measures the real height for you, which CSS transitions cannot do.
Motion components use browser APIs, so the file that renders them needs "use client". A server component can still render that client component and pass it children.
Not when you stick to transforms and opacity, which run without re-layout. Large layout animations and many simultaneous elements cost more; test on a mid-range phone and use LazyMotion to trim the bundle.
Related: CSS animation generator, React Router examples, React icons, UI components, All free tools.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.