Добавить в корзинуПозвонить
Найти в Дзене
Vibecode Wiki

Framer Motion за один вечер: анимируй React-компоненты как профи

В середине 2025 года библиотека стала независимой и переименовалась из framer-motion в motion. Пакет framer-motion по-прежнему работает и обновляется, но новые проекты лучше стартовать с нового имени: npm install motion Импорт в React: // Новые проекты import { motion, AnimatePresence } from "motion/react" // Старые проекты — работает без изменений import { motion, AnimatePresence } from "framer-motion" Для Next.js App Router — обязательно "use client" в начале файла: Motion работает только на клиенте. Вся библиотека строится на трёх пропсах: initial={{ opacity: 0, y: 20 }} // начальное состояние animate={{ opacity: 1, y: 0 }} // целевое состояние exit={{ opacity: 0, y: -20 }} // состояние при выходе transition={{ duration: 0.4, ease: "easeOut" }} /> initial — откуда. animate — куда. exit — как уходит. Браузер сам интерполирует между ними. Главное правило производительности: анимируй только opacity и transform (x, y, scale, rotate, skew). Никогда width, height, top, left — это reflow
Оглавление

Установка в 2026: motion, не framer-motion

В середине 2025 года библиотека стала независимой и переименовалась из framer-motion в motion. Пакет framer-motion по-прежнему работает и обновляется, но новые проекты лучше стартовать с нового имени:

npm install motion

Импорт в React:

// Новые проекты

import { motion, AnimatePresence } from "motion/react"

// Старые проекты — работает без изменений

import { motion, AnimatePresence } from "framer-motion"

Для Next.js App Router — обязательно "use client" в начале файла: Motion работает только на клиенте.

Базовая механика: три пропса которые решают всё

Вся библиотека строится на трёх пропсах:

initial={{ opacity: 0, y: 20 }} // начальное состояние

animate={{ opacity: 1, y: 0 }} // целевое состояние

exit={{ opacity: 0, y: -20 }} // состояние при выходе

transition={{ duration: 0.4, ease: "easeOut" }}

/>

initial — откуда. animate — куда. exit — как уходит. Браузер сам интерполирует между ними.

Главное правило производительности: анимируй только opacity и transform (x, y, scale, rotate, skew). Никогда width, height, top, left — это reflow на каждом кадре.

// Правильно

// Медленно

Варианты: оркестрация без хаоса

Когда анимируется один элемент — инлайн-объекты работают. Когда экран с десятью карточками каждая с тремя состояниями — инлайн становится нечитаемым. Варианты выносят это за пределы 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 (

variants={container}

initial="hidden"

animate="visible"

>

{cards.map(card => (

))}

)

}

Когда родитель переходит в состояние "visible", дочерние motion.li автоматически делают то же — но с staggerChildren: 0.08, то есть с задержкой 80мс между каждым. Это и есть оркестрация: родитель командует, дети выполняют.

Важно: объект вариантов объявляй вне компонента. Если объявить внутри — он пересоздаётся на каждый рендер и Motion запускает анимацию заново.

AnimatePresence: анимация выхода

Проблема которая заставляет использовать Motion а не чистый CSS: React удаляет компонент из DOM мгновенно. exit проп никогда не успевает сыграть без AnimatePresence:

import { AnimatePresence, motion } from "motion/react"

function Notification({ message, isVisible }) {

return (

{isVisible && (

key="notification"

initial={{ opacity: 0, y: -16 }}

animate={{ opacity: 1, y: 0 }}

exit={{ opacity: 0, y: -16 }}

transition={{ duration: 0.25 }}

>

{message}

)}

)

}

AnimatePresence отслеживает дочерние элементы. Когда isVisible становится false — он даёт компоненту сыграть exit перед удалением из DOM.

Два правила AnimatePresence:

Первое — у каждого дочернего элемента должен быть уникальный key. Без него Motion не понимает что именно ушло.

Второе — mode="wait" если нужно дождаться выхода одного элемента перед входом следующего:

{step === 1 && }

{step === 2 && }

Layout-анимации: позиция и размер сами обновляются

Самая магическая фича. Добавь layout проп — и Motion автоматически анимирует изменение позиции и размера элемента при ре-рендере. Никакого ручного расчёта координат:

function FilteredList({ items }) {

return (

{items.map(item => (

key={item.id}

layout // вот и всё

transition={{ duration: 0.3 }}

>

{item.name}

))}

)

}

При фильтрации списка элементы плавно перемещаются на новые позиции. Без layout — они прыгают мгновенно.

layoutId — для Shared Element Transitions: один элемент «перетекает» в другой даже если это разные компоненты в разных частях дерева:

// Карточка в сетке

setSelected(id)} />

// Развёрнутая карточка в модалке

Motion сам находит элементы с одинаковым layoutId и анимирует переход между ними.

Жесты: hover, tap, drag

whileHover={{ scale: 1.05, backgroundColor: "#e94560" }}

whileTap={{ scale: 0.97 }}

transition={{ duration: 0.15 }}

>

Нажми меня

whileHover и whileTap — декларативные жесты. Не нужны onMouseEnter/onMouseLeave с состоянием.

Drag — три строки:

drag

dragConstraints={{ left: -100, right: 100, top: -50, bottom: 50 }}

dragElastic={0.2}

/>

dragElastic — насколько элемент выходит за dragConstraints перед возвратом. 0 — жёсткие ограничения. 1 — свободно. 0.2 — небольшой пружинящий эффект.

LazyMotion: бандл с 34кб до 6кб

motion.div приносит весь Motion в бандл — ~34кб. Для большинства проектов нормально. Для критичного первого рендера — много:

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

{/* m.div вместо motion.div — работает только внутри LazyMotion */}

)

}

LazyMotion с асинхронной загрузкой domAnimation — начальный бандл ~6кб, фичи грузятся после. domMax вместо domAnimation добавляет drag и layout — ~18кб.

useReducedMotion: уважай настройки пользователя

import { useReducedMotion } from "motion/react"

function AnimatedCard({ children }) {

const shouldReduceMotion = useReducedMotion()

return (

initial={{ opacity: 0, y: shouldReduceMotion ? 0 : 20 }}

animate={{ opacity: 1, y: 0 }}

transition={{ duration: shouldReduceMotion ? 0 : 0.4 }}

>

{children}

)

}

Или глобально через MotionConfig — один раз на весь проект:

import { MotionConfig } from "motion/react"

function App() {

return (

{/* Все motion-компоненты внутри автоматически

уважают prefers-reduced-motion */}

)

}

reducedMotion="user" — следует системной настройке. reducedMotion="always" — отключает везде. reducedMotion="never" — всегда включено.

Три промпта для агентов

Анимированная карточка с stagger:

Создай компонент AnimatedGrid который принимает массив items.

Используй motion из "motion/react".

При монтировании карточки появляются снизу с stagger 80мс:

- Контейнер: variants с staggerChildren: 0.08

- Каждая карточка: initial opacity 0 + y 20, animate opacity 1 + y 0

- transition: duration 0.4, ease "easeOut"

Объекты вариантов объяви вне компонента.

Добавь MotionConfig reducedMotion="user" на уровне компонента.

AnimatePresence для роутинга страниц:

Добавь анимацию переходов между страницами в Next.js App Router.

Создай компонент PageTransition который оборачивает children.

Использует AnimatePresence mode="wait".

Анимация: текущая страница fade out за 0.2s, новая fade in за 0.2s.

Не использовать framer-motion — только "motion/react".

Компонент должен быть "use client".

Shared Element Transition для карточки-модалки:

Сделай переход карточка → модалка через layoutId.

Есть сетка карточек. При клике на карточку открывается модалка

с той же карточкой развёрнутой на весь экран.

Используй motion из "motion/react" и layoutId={`card-${id}`}

на обоих элементах.

Обёртка модалки — AnimatePresence.

Закрытие по клику на оверлей.

Плавный переход позиции и размера — только через layout анимацию,

не через CSS position/transform вручную.

useScroll и useTransform: parallax и прогресс

useScroll возвращает MotionValue — реактивное значение которое обновляется при скролле без ре-рендера компонента:

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 (

Заголовок

)

}

useTransform — маппинг одного диапазона в другой. Первый аргумент — MotionValue, второй — входной диапазон, третий — выходной. Можно маппировать в цвета, строки, любые CSS-значения.

Для scroll-прогресса элемента во вьюпорте — useScroll с 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 (

Контент

)

}

offset: ["start end", "end start"] — отслеживать от момента когда верх элемента достигает низа вьюпорта до момента когда низ элемента уходит за верх. scrollYProgress идёт 0→1 за это время.

useAnimate: императивные анимации вне JSX

Иногда нужно запустить анимацию по событию, не по изменению состояния. useAnimate даёт scope и функцию animate для императивного управления:

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 (

)

}

animate возвращает Promise — можно ждать окончания через await и запускать следующее действие. Это удобно для последовательностей: сначала подсветить ошибку, потом скрыть сообщение, потом сбросить поле.

useAnimate также позволяет анимировать дочерние элементы по селектору:

await animate("li", { opacity: 0 }, { delay: stagger(0.05) })

stagger — вспомогательная функция для задержки между элементами, работает внутри useAnimate так же как staggerChildren в вариантах.

Пять ошибок которые делают все

Объявляют варианты внутри компонента. При каждом рендере создаётся новый объект, Motion думает что варианты изменились и запускает анимацию снова. Выноси наружу всегда.

Забывают key в AnimatePresence. Без уникального ключа exit-анимация не играет — React не понимает что элемент заменился.

Анимируют width и height вместо scale. Для большинства «раскрывающихся» эффектов scale быстрее и плавнее — он не вызывает reflow.

Используют motion.div везде когда нужен LazyMotion. Если проект крупный и бандл важен — один раз настроить LazyMotion с m.div и забыть.

Не оборачивают в MotionConfig reducedMotion="user". Это две строки которые делают весь проект доступным для пользователей с вестибулярными расстройствами.

Источник и полная версия: VibeCode Wiki