Overview
Motion is the production animation library for React (previously published as framer-motion). Animations are declared as props on motion.* components: initial, animate, exit, whileHover, whileTap, whileInView, layout, layoutId and variants. Springs are the default for physical values, and transform and opacity animations run on the GPU. Version 14.0.0 was published on 2026-10-02; 12.x and 13.x code keeps working for the APIs shown here, but check the changelog if you relied on internal framer-motion APIs.
Instructions
Install and import
npm install motion
import { motion, AnimatePresence } from "motion/react";
- The old package
framer-motionstill exists, but new code should usemotionand import frommotion/react. Migrating means swapping the dependency and changingfrom "framer-motion"tofrom "motion/react". - Requires React 18.2 or newer.
- Next.js App Router: components using
motionneed"use client"at the top of the file, or importimport * as motion from "motion/react-client"so they can render inside server components. - Bundle size: the
motioncomponent is about 34 kB; useLazyMotionwith themcomponent (import * as m from "motion/react-m") anddomAnimation(+15 kB: animations, variants, exits, gestures) ordomMax(+25 kB: adds drag and layout animations). - Outside React,
import { animate, scroll } from "motion"provides the same engine for plain DOM; inside components,useAnimatereturns a scope and ananimatefunction for imperative sequences.
Accessibility
Wrap the app in <MotionConfig reducedMotion="user"> so transform and layout animations are disabled for people whose OS asks for reduced motion (opacity and colour animations are kept). useReducedMotion() returns the boolean for custom handling, for example setting a parallax y to 0.
Patterns
Basic Animations
import { motion, AnimatePresence } from "motion/react";
// Animate on mount
function FadeIn({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 20 }} // Starting state
animate={{ opacity: 1, y: 0 }} // Target state
transition={{ duration: 0.5, ease: "easeOut" }}
>
{children}
</motion.div>
);
}
// Hover and tap interactions
function InteractiveCard({ title }: { title: string }) {
return (
<motion.div
className="card"
whileHover={{ scale: 1.05, boxShadow: "0 10px 30px rgba(0,0,0,0.12)" }}
whileTap={{ scale: 0.95 }}
transition={{ type: "spring", stiffness: 300, damping: 20 }}
>
<h3>{title}</h3>
</motion.div>
);
}
// Exit animations
function NotificationList({ notifications }: { notifications: Notification[] }) {
return (
<AnimatePresence>
{notifications.map((n) => (
<motion.div
key={n.id}
initial={{ opacity: 0, x: 100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -100, height: 0 }} // Animate out!
transition={{ type: "spring", damping: 25 }}
>
{n.message}
</motion.div>
))}
</AnimatePresence>
);
}
Layout Animations
// Automatic layout animation
function ExpandableCard({ isExpanded, onClick, children }: Props) {
return (
<motion.div
layout // Animate ANY layout change
onClick={onClick}
style={{
width: isExpanded ? 400 : 200,
height: isExpanded ? 300 : 100,
}}
transition={{ layout: { type: "spring", stiffness: 200 } }}
>
<motion.h3 layout="position">{/* Only animate position, not size */}</motion.h3>
<AnimatePresence>
{isExpanded && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
{children}
</motion.div>
)}
</AnimatePresence>
</motion.div>
);
}
// Shared layout animation (element moves between components)
function TabLayout({ activeTab }: { activeTab: string }) {
return (
<div className="tabs">
{tabs.map((tab) => (
<button key={tab.id} onClick={() => setActive(tab.id)}>
{tab.label}
{activeTab === tab.id && (
<motion.div
layoutId="activeTab" // Same layoutId = shared animation
className="underline"
transition={{ type: "spring", stiffness: 500, damping: 30 }}
/>
)}
</button>
))}
</div>
);
}
Scroll Animations
import { motion, useScroll, useTransform } from "motion/react";
function ParallaxHero() {
const { scrollY } = useScroll();
const y = useTransform(scrollY, [0, 500], [0, -150]);
const opacity = useTransform(scrollY, [0, 300], [1, 0]);
return (
<motion.div style={{ y, opacity }} className="hero">
<h1>Welcome</h1>
</motion.div>
);
}
// Scroll-triggered entrance
function ScrollReveal({ children }: { children: React.ReactNode }) {
return (
<motion.div
initial={{ opacity: 0, y: 50 }}
whileInView={{ opacity: 1, y: 0 }} // Animate when in viewport
viewport={{ once: true, margin: "-100px" }}
transition={{ duration: 0.6 }}
>
{children}
</motion.div>
);
}
Staggered Children
const container = {
hidden: { opacity: 0 },
show: {
opacity: 1,
transition: { staggerChildren: 0.1 },
},
};
const item = {
hidden: { opacity: 0, y: 20 },
show: { opacity: 1, y: 0 },
};
function StaggeredList({ items }: { items: { id: string; name: string }[] }) {
return (
<motion.ul variants={container} initial="hidden" animate="show">
{items.map((i) => (
<motion.li key={i.id} variants={item}>
{i.name}
</motion.li>
))}
</motion.ul>
);
}
Examples
Example 1: Animated notification stack
User request: "Make toast notifications slide in from the right and slide out when dismissed, without the list jumping."
Use the exit-animation pattern above and add mode="popLayout" to AnimatePresence so exiting items leave the layout flow, plus layout on each item so siblings glide into the gap:
<AnimatePresence mode="popLayout">
{toasts.map((t) => (
<motion.div key={t.id} layout
initial={{ opacity: 0, x: 100 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: 100 }}>
{t.message}
</motion.div>
))}
</AnimatePresence>
Result: new toasts slide in, dismissed ones slide out, remaining toasts move up smoothly. Keys must be stable ids, never array indexes.
Example 2: Scroll progress bar and reveal-on-scroll sections
User request: "Add a reading-progress bar at the top and fade in each section as it scrolls into view."
import { motion, useScroll, useSpring } from "motion/react";
export function ProgressBar() {
const { scrollYProgress } = useScroll();
const scaleX = useSpring(scrollYProgress, { stiffness: 120, damping: 30 });
return <motion.div style={{ scaleX, transformOrigin: "0 0" }} className="fixed inset-x-0 top-0 h-1 bg-emerald-500" />;
}
Wrap each section in the ScrollReveal component above (whileInView, viewport={{ once: true }}). Result: a bar that fills as the page scrolls and sections that fade up once.
Guidelines
layoutprop: add it to an element and size or position changes animate automatically. Uselayout="position"on text children to avoid stretching, andlayouton children of a resized parent to fix scale distortion. Scrollable ancestors needlayoutScroll; fixed ancestors needlayoutRoot. Wrap components that do not re-render together inLayoutGroup.- AnimatePresence: it must wrap the conditional, not be wrapped by it; direct children need unique, stable
keys. Modes:sync(default),wait(enter after exit finishes),popLayout.initial={false}skips the first-render animation. - Springs:
type: "spring"withstiffness(speed) anddamping(bounce); aduration/bouncepair is easier to tune by feel. - Scroll:
whileInViewfor entrance effects (withviewport={{ once: true }}),useScrollplususeTransformfor scroll-linked effects. Prefer these over scroll listeners withsetState. - Shared layout: the same
layoutIdon two elements animates between them (tab underlines, card-to-modal); makelayoutIds unique per group. - Gestures:
whileHover,whileTap,whileDrag,whileFocus; hover effects do not fire on touch. - Performance: animate
transformandopacity; animatingwidth,heightortopforces layout, so uselayoutinstead. Avoid re-rendering the parent per frame; use motion values (useMotionValue,useTransform) which update without renders. - Do not use for simple one-off hover colours (CSS transitions are lighter) or for server-only components without the client import noted above.