~/wiki / motion-i-animatsiya / framer-motion-prakticheskiy-gayd

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.

$ cd section/ $ join vibe dev
Framer Motion in One Evening: Animate React Components as Pros - обложка

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

bash
npm install motion

Import to React:

javascript
// New projects
import {motion, AnimatePresence } from "motion/react"

Old ProjectsWorks 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:

javascript
<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.

javascript
// Правильно
<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:

javascript
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:

javascript
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:

javascript
<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

javascript
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:

javascript
// 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

javascript
<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:

javascript
<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:

javascript
// 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

javascript
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:

javascript
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:

markdown
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:

markdown
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:

markdown
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:

javascript
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:

javascript
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:

javascript
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:

javascript
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.

$ cd ../ ← back to Motion and animation