Установка в 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