Framer Motion in One Evening: Animate React Components as Pros
Main chat
A chat for vibe coders: news, guides, live cases, marketplace, and finding executors.
Installation in 2026: motion, not framer-motion
In mid-2025, the library became independent and renamed from framer-motion to motion. framer-motion is still up and running, but it’s better to start with a new name
npm install motion
Import to React:
// New projects
import {motion, AnimatePresence } from "motion/react"
Old Projects – Works Unchanged
import { motion, AnimatePresence } from "framer-motion"
For Next.js App Router, "use client" is required at the beginning of the file: Motion only works on the client.
Basic mechanics: three props that solve everything
The entire library is built on three props:
<motion.div
initial={{ opacity: 0, y: 20 }} // начальное состояние
animate={{ opacity: 1, y: 0 }} // целевое состояние
exit={{ opacity: 0, y: -20 }} // состояние при выходе
transition={{ duration: 0.4, ease: "easeOut" }}
/>
initial - from where. animate - where to go. exit - as it goes. The browser itself interpolates between them.
The main rule of performance: animate only opacity and transform (x, y, scale, rotate, skew). Never width, height, top, left is a reflow on every frame.
// Правильно
<motion.div animate={{ x: 100, opacity: 0.5, scale: 0.9 }} />
// Медленно
<motion.div animate={{ left: 100, width: "50%" }} />
Options: orchestration without chaos
When one element is animated, the online objects work. When a screen with ten cards each with three states, online becomes unreadable. Options take this beyond JSX:
const container = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.08, // задержка между дочерними
delayChildren: 0.2 // задержка до первого дочернего
}
}
}
const item = {
hidden: { opacity: 0, y: 16 },
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" }
}
}
function CardGrid() {
return (
<motion.ul
variants={container}
initial="hidden"
animate="visible"
>
{cards.map(card => (
<motion.li key={card.id} variants={item}>
<Card {...card} />
</motion.li>
))}
</motion.ul>
)
}
When a parent transitions to "visible", daughters of motion.li automatically do the same – but with staggerChildren: 0.08, that is, with an 80ms delay between each. This is orchestration: the parent commands, the children execute.
Important: the object of options declare **out of the component **. If you announce it inside, it recreates for each render, and Motion runs the animation anew.
AnimatePresence: Animation of the release
A problem that forces you to use Motion rather than pure CSS: React removes a component from the DOM instantly. exit prop never has time to play without AnimatePresence:
import { AnimatePresence, motion } from "motion/react"
function Notification({ message, isVisible }) {
return (
<AnimatePresence>
{isVisible && (
<motion.div
key="notification"
initial={{ opacity: 0, y: -16 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -16 }}
transition={{ duration: 0.25 }}
>
{message}
</motion.div>
)}
</AnimatePresence>
)
}
AnimatePresence monitors child elements. When isVisible becomes false, it lets the component play exit before being removed from the DOM.
Two rules of AnimatePresence:
First, each child element must have a unique key. Without it, Motion doesn't know what's gone.
The second is mode="wait" if you need to wait for the release of one element before entering the following:
<AnimatePresence mode="wait">
{step === 1 && <motion.div key="step1" ... />}
{step === 2 && <motion.div key="step2" ... />}
</AnimatePresence>
Layout animations: position and size are updated
The most magical thing. Add layout prop and Motion automatically animates the change in position and size of the item when re-rendered. No manual coordinate calculation
function FilteredList({ items }) {
return (
<ul>
{items.map(item => (
<motion.li
key={item.id}
layout // вот и всё
transition={{ duration: 0.3 }}
>
{item.name}
</motion.li>
))}
</ul>
)
}
When filtering the list, elements move smoothly to new positions. Without layout, they jump instantly.
layoutId for Shared Element Transitions: One element "flows" into another, even if it is different components in different parts of the tree:
// Grid card
<motion.div layoutId={`card-${id}`} onClick={() => setSelected(id)} />
// Deployed card in the modal
<motion.div layoutId={`card-${selected}`} />
Motion finds elements with the same layoutId and animates the transition between them.
Gestures: hover, tap, drag
<motion.button
whileHover={{ scale: 1.05, backgroundColor: "#e94560" }}
whileTap={{ scale: 0.97 }}
transition={{ duration: 0.15 }}
>
Нажми меня
</motion.button>
whileHover and whileTap are declarative gestures. onMouseEnter/onMouseLeave is not required.
Drag -- three lines:
<motion.div
drag
dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}
dragElastic={0.2}
/>
dragElastic: How far an item goes beyond dragConstraints before returning. 0 is a tight constraint. 1 is free. 0.2 is a small springing effect.
LazyMotion: bundle from 34kb to 6kb
motion.div brings all Motion to the bundle - ~34kb. Most projects are fine. For the critical first render, there are many:
// features.js — отдельный чанк
export { domAnimation as default } from "motion/react"
// App.jsx
import { LazyMotion, m } from "motion/react"
const loadFeatures = () => import("./features").then(r => r.default)
function App() {
return (
<LazyMotion features={loadFeatures}>
{/* m.div вместо motion.div — работает только внутри LazyMotion */}
<m.div animate={{ opacity: 1 }} />
</LazyMotion>
)
}
LazyMotion with asynchronous boot domAnimation - initial bundle ~6kb, features are loaded after. domMax instead of domAnimation adds drag and layout - ~18kb.
useReducedMotion: Respect user settings
import { useReducedMotion } from "motion/react"
function AnimatedCard({ children }) {
const shouldReduceMotion = useReducedMotion()
return (
<motion.div
initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: shouldReduceMotion ? 0 : 0.4 }}
>
{children}
</motion.div>
)
}
Or globally through MotionConfig - once for the entire project:
import { MotionConfig } from "motion/react"
function App() {
return (
<MotionConfig reducedMotion="user">
{/* Все motion-компоненты внутри автоматически
уважают prefers-reduced-motion */}
<Router />
</MotionConfig>
)
}
reducedMotion="user" – follows system configuration. reducedMotion="always" shuts down everywhere. reducedMotion="never" is always on.
Three prompts for agents
Animated card with stagger:
Create an AnimatedGrid component that accepts an array of items.
Use Motion from Motion/React.
When mounting, the cards appear from the bottom with stagger 80ms:
Container: variants with StaggerChildren: 0.08
Each card: initial opacity 0 + y 20, animate opacity 1 + y 0
- transition: duration 0.4, ease "easeOut"
Options are declared outside the component.
Add MotionConfig reducedMotion="user" at the component level.
AnimatePresence for routing pages:
Add an animation of transitions between pages in Next.js App Router.
Create a PageTransition component that wraps children.
It uses AnimatePresence mode=wait.
Animation: current page fade out for 0.2s, new fade in for 0.2s.
Do not use framer-motion – only “motion/react”.
The component must be a “use client”.
Shared Element Transition for a modular card:
Make the transition card → modal through layoutId.
There's a grid of cards. When you click on the card, the modal opens.
With the same card spread all over the screen.
Use motion from "motion/react" and layoutId={`card-${id}`}
on both elements.
The wrapper is AnimatePresence.
Closing by click on overlay.
Smooth transition of position and size - only through layout animation.
Not through CSS position/transform manually.
useScroll and useTransform: parallax and progress
useScroll returns MotionValue, a reactive value that is updated when scrolled without re-rendering the component:
import { useScroll, useTransform, motion } from "motion/react"
function ParallaxHero() {
const { scrollY } = useScroll()
// scrollY 0→300 → translateY 0→-100
const y = useTransform(scrollY, [0, 300], [0, -100])
const opacity = useTransform(scrollY, [0, 200], [1, 0])
return (
<motion.div style={{ y, opacity }}>
<h1>Заголовок</h1>
</motion.div>
)
}
useTransform is a mapping of one range to another. The first argument is MotionValue, the second is the input range, the third is the output. You can map to colors, lines, any CSS values.
For the scroll progression of the element in the futureport - useScroll with target:
function AnimatedSection() {
const ref = useRef(null)
const { scrollYProgress } = useScroll({
target: ref,
offset: ["start end", "end start"]
})
const scale = useTransform(scrollYProgress, [0, 0.5], [0.8, 1])
return (
<motion.section ref={ref} style={{ scale }}>
Контент
</motion.section>
)
}
offset: ["start end", "end start"] - track from the moment when the top of the element reaches the bottom of the present port to the moment when the bottom of the element goes beyond the top. scrollYProgress goes 0→1 during this time.
useAnimate: Imperative animations outside JSX
Sometimes you need to run an animation based on an event, not on a change in state. useAnimate gives scope and the animate function for imperative control:
import { useAnimate } from "motion/react"
function ShakeButton() {
const [scope, animate] = useAnimate()
async function handleWrongInput() {
// Анимация тряски при ошибке ввода
await animate(scope.current, { x: [-8, 8, -8, 8, 0] }, { duration: 0.4 })
}
return (
<div ref={scope}>
<input onBlur={handleWrongInput} />
</div>
)
}
animate returns Promise - you can wait for the end using await and run the next action. This is convenient for sequences: first highlight the error, then hide the message, then reset the field.
useAnimate also allows you to animate child elements by selector:
await animate("li", { opacity: 0 }, { delay: stagger(0.05) })
stagger, an auxiliary function for delay between elements, works within useAnimate as well as staggerChildren in variants.
Five mistakes everyone makes
Announce options inside the component. With each render, a new object is created, Motion thinks the options have changed and runs the animation again. Always take it out.
Forget key in AnimatePresence. Without the unique key exit animation does not play - React does not understand that the element has been replaced.
width and height are animated instead of scale. For most of the "opening" effects, scale is faster and smoother - it does not cause reflow.
** Use motion.div wherever LazyMotion is needed.** If the project is large and the bundle is important, configure LazyMotion with m.div once and forget.
Not wrapped in MotionConfig reducedMotion="user". These are two lines that make the entire project accessible to users with vestibular disorders.