
Что представляет из себя данная статья?
Когда я начал разбираться с Motion для React, то оказалось, что свежих обзорных статей почти нет — нашёл только несколько старых постов про framer-motion. Поэтому я решил написать свой обзор: перевёл и разобрал документацию (ссылки в конце), попробовал библиотеку в деле и собрал всё в одном месте. В статье есть примеры кода, GIF-анимации и описание хуков, которых, по моему личному мнению, достаточно, чтобы понять Motion, и, возможно, попробовать его руками, сэкономив время на чтении документации.
Тестовый проект: анимированный TODO-лист
Покажу Motion в деле на примере интерактивного TODO-листа. К стандартным функциям добавлена анимация появления, исчезновения и перетаскивания задач.

Для начала создадим базовый проект (на примере, Vite):
npm create vite@latest my-motion-app -- --template react
cd my-motion-app
npm install
npm install motion
// src/main.jsx
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { MotionConfig } from 'motion/react';
import './index.css';
import App from './App.jsx';
createRoot(document.getElementById('root')).render(
<StrictMode>
<MotionConfig
reducedMotion="user"
layout
transition={{ type: 'tween', ease: 'easeInOut', duration: 0.25 }}
>
<App />
</MotionConfig>
</StrictMode>,
);
Сам проект с интерактивным демо доступен по ссылке:
Вот код нашего анимированного TODO-листа находится...
...под спойлером
// src/components/TodoList.jsx
import { useState, useRef, useEffect } from 'react';
import { Reorder, AnimatePresence, motion, useScroll, useTransform } from 'motion/react';
import styles from './styles.module.css';
const initialTodos = [
{ id: 1, text: 'Почитать доку Motion' },
{ id: 2, text: 'Добавить анимацию в TODO' },
];
function TodoList() {
const [todos, setTodos] = useState(initialTodos);
const [newTodoText, setNewTodoText] = useState('');
const containerRef = useRef(null);
const [isScrolled, setIsScrolled] = useState(false);
const { scrollYProgress } = useScroll({ container: containerRef });
const scaleX = useTransform(scrollYProgress, [0, 1], [0, 1]);
useEffect(() => {
const el = containerRef.current;
if (!el) return;
const onScroll = () => setIsScrolled(el.scrollTop > 0);
el.addEventListener('scroll', onScroll, { passive: true });
return () => el.removeEventListener('scroll', onScroll);
}, []);
const addTodo = () => {
if (newTodoText.trim() === '') return;
const newId = Math.max(0, ...todos.map((t) => parseInt(t.id))) + 1;
setTodos([{ id: newId, text: newTodoText }, ...todos]);
setNewTodoText('');
};
const removeTodo = (idToRemove) => {
setTodos(todos.filter((todo) => todo.id !== idToRemove));
};
const onKeyDown = (e) => {
if (e.key === 'Enter') addTodo();
};
const canAdd = newTodoText.trim() !== '';
return (
<motion.div className={styles.container}>
<motion.div className={styles.inputArea}>
<motion.input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Что еще нужно сделать?"
className={styles.input}
whileFocus={{ boxShadow: '0 0 0 2px #007bff' }}
whileHover={{ backgroundColor: '#f0f0f0' }}
/>
<motion.button
onClick={addTodo}
className={styles.addButton}
whileTap={{ scale: 0.95 }}
whileHover={{ scale: 1.05, backgroundColor: '#0056b3' }}
disabled={!canAdd}
>
Добавить
</motion.button>
</motion.div>
<motion.div ref={containerRef} className={styles.listScrollArea}>
<AnimatePresence>
{isScrolled ? (
<motion.div
key="scroll-indicator"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={styles.scrollIndicator}
style={{ scaleX, transformOrigin: '0% 0%' }}
/>
) : null}
</AnimatePresence>
<Reorder.Group
axis="y"
values={todos}
onReorder={setTodos}
layoutScroll
className={styles.reorderGroup}
>
{todos.length === 0 ? (
<motion.div
className={styles.emptyState}
initial={{ opacity: 0, x: -200 }}
animate={{ opacity: 1, x: 0 }}
>
Список пуст
</motion.div>
) : (
<AnimatePresence mode="popLayout">
{todos.map((item) => (
<Reorder.Item
key={item.id}
value={item}
initial={{ opacity: 0, x: -200 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 200 }}
transition={{ ease: 'easeOut' }}
whileHover={{ backgroundColor: '#f0f0f0' }}
whileTap={{ scale: 0.98 }}
className={styles.item}
>
<motion.span>{item.text}</motion.span>
<motion.button
onClick={() => removeTodo(item.id)}
className={styles.deleteButton}
whileTap={{ scale: 0.8 }}
whileHover={{ scale: 1.2, color: '#ce0000' }}
transition={{ duration: 0.1, ease: 'easeOut' }}
aria-label="Удалить задачу"
>
×
</motion.button>
</Reorder.Item>
))}
</AnimatePresence>
)}
</Reorder.Group>
</motion.div>
</motion.div>
);
}
export default TodoList;
Давайте разбираться, что там есть и как работает.
Основные компоненты библиотеки
Когда мы говорим о Motion, то в первую очередь подразумеваем motion-компоненты. Это, по сути, обёртки над обычными HTML- или SVG-элементами. Добавьте motion.
перед любым тегом, скажем <motion.div>
или <motion.circle>, и он сразу же обретёт способность к анимации.
Эти motion-компоненты принимают несколько ключевых свойств, которые и определяют анимацию:
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.2 }}
style={{ scaleX, transformOrigin: '0% 0%' }}
/>
1. initial
: Это свойство задаёт начальное состояние вашего элемента, откуда начнётся анимация.
<motion.div
initial={{ opacity: 0, x: -100 }} // Элемент сначала невидимый и смещен влево
// ...
/>
Если вы его не укажете, Motion попытается сам «догадаться» о стартовой точке. Иногда бывает удобно поставить initial={false}
, чтобы отключить анимацию при первом рендере, если элемент уже на месте.
<motion.div initial={false} animate={{ opacity: 0 }} />
2. animate
: ключевой элемент, собственно. Здесь вы указываете, куда должен «прилететь» ваш компонент.
<motion.div
// ...
animate={{ opacity: 1, x: 0 }} // Становится видимым и перемещается в свою позицию
// ...
/>
Motion сам плавно переведёт его из состояния initial
(или текущего) в целевое.
3. transition
: с этим свойством мы настраиваем то, как именно будет происходить анимация: её длительность (duration
), плавность (easing
), задержка (delay
) и даже тип анимации (например, "tween"
для простого линейного движения или "spring"
для упругих, более реалистичных эффектов).
<motion.div
// ...
transition={{ duration: 0.2 }} // Анимация длится 0.2 секунды
// ...
/>
4. style
: Важный нюанс: свойство style
в motion-компонентах поддерживает независимые трансформации. Это значит, что вы можете управлять transform-свойствами (типа x, y, scale, rotate) через Motion Values (о которых поговорим далее), и они будут обновляться напрямую в DOM, не вызывая лишних ререндеров React-компонента. Это даёт больше свободы для сложных «визуальных трюков» и повышает производительность.
<motion.div
// ...
style={{ scaleX, transformOrigin: '0% 0%' }} // scaleX управляется useTransform, анимируя ширину
// ...
/>
Весь подход — декларативный, что убирает головную боль по управлению DOM и таймингами. Код быстрее, чище, меньше багов.
Оптимизация загрузки с LazyMotion.
Есть классная фишка для оптимизации веса бандла — LazyMotion
. «Полный» motion-компонент может весить около 36 КБ.

Но с LazyMotion
вы можете загрузить только тот минимум, который вам действительно нужен (например, domAnimation
для базовых анимаций), сократив размер до ~6 КБ. Это заметно влияет на скорость загрузки страницы.
Как использовать:
import { LazyMotion, domAnimation } from "motion/react"
import * as m from "motion/react-m"
function MyAnimatedComponent() {
return (
<LazyMotion features={domAnimation} strict>
<m.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.5 }}
>
LazyMotion
</m.div>
</LazyMotion>
);
}
Кроме синхронной загрузки, LazyMotion
поддерживает и асинхронную. Это особенно полезно, если вы хотите отложить подключение анимационных функций до полной инициализации страницы, сохранив интерфейс максимально лёгким на старте.
Пример асинхронной загрузки:
// features.js
import { domAnimation } from "motion/react"
export default domAnimations
// index.js
const loadFeatures = import("./features.js")
.then(res => res.default)
function Component() {
return (
<LazyMotion features={loadFeatures}>
<m.div animate={{ scale: 1.5 }} />
</LazyMotion>
)
}js
Важно: внутри LazyMotion
нужно использовать m.div
, а не motion.div
. Иначе бандл подтянет весь вес сразу, и ленивой загрузки не будет. Если включить флаг strict
(по умолчанию, false
), библиотека автоматически выдаст ошибку при неправильном использовании, что удобно для отладки.
AnimatePresence
Компонент AnimatePresence
— это настоящий спаситель, когда дело доходит до плавной анимации элементов при их удалении из DOM. По умолчанию React просто мгновенно убирает компоненты, что выглядит резко. AnimatePresence
позволяет нашим motion-компонентам красиво «попрощаться» перед исчезновением.

Как это работает? AnimatePresence
отслеживает, когда его непосредственные дочерние элементы готовятся покинуть React-дерево. Это происходит в нескольких распространенных сценариях:
Условный рендеринг: Вы показываете или скрываете компонент с помощью условия, например:
{show && <Modal />}
. Когда show становитсяfalse
, AnimatePresence перехватывает его уход.Изменение
key
: Если у дочернего компонента изменяется пропсkey
, React воспринимает его как новый элемент, а старый удаляет. Это идеальное решение для анимированных каруселей или переключающихся вкладок (табов), где один элемент сменяет другой.Удаление из списка: Элементы удаляются из динамического списка motion-компонентов, как мы видим в нашем примере с TODO-листом.
Свойство exit
: анимированное исчезновение компонента
Чтобы компонент красиво исчез, мы используем свойство exit
. Оно работает точно так же, как свойство animate
, но применяется к прощальной анимации. Вы задаете конечные значения (например, exit={{ opacity: 0, y: 100 }}
), и компонент плавно анимируется к ним, прежде чем окончательно удалиться из DOM.
<AnimatePresence initial={false}>
{hasScroll && (
<motion.div
initial={{ opacity: 0, x: -100 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: 100 }}
transition={{ duration: 0.2 }}
style={{ scaleX, transformOrigin: '0% 0%' }}
/>
)}
</AnimatePresence>
Плавные изменения расположения и размера элементов: layout и LayoutGroup
Когда элементы на странице меняют свой размер или позицию — например, при добавлении/удалении из списка, сортировке или просто расширении какого-то блока — это может выглядеть резко. Motion умеет делать эти изменения супер плавными и красивыми. За это отвечают свойства layout
и компонент LayoutGroup
.
layout
Если вы добавите свойство layout
к любому motion-компоненту, он будет автоматически и плавно анимировать свои размеры и положение при любых изменениях. Motion сам разруливает все сложности под капотом, включая "inverse transform"
(коррекцию масштаба), чтобы элементы не «прыгали» и не мерцали.
<motion.div style={{ width: isExpanded ? 200 : 100 }}
layout
transition={{
type: 'tween',
transition: {
duration: 0.5,
ease: 'easeInOut'
}
}}
/>

LayoutGroup
Что делать, если элементы, которые должны «знать» о движениях друг друга, не являются прямыми соседями в DOM? Тут на помощь приходит LayoutGroup
. Просто оберните эти элементы в LayoutGroup
, и они начнут «общаться» по поводу своих изменений положения и размеров, позволяя Motion координировать анимации даже между несвязанными компонентами.
Код:
function AccordionItem({ header, content }) {
const [isOpen, setIsOpen] = useState(false);
return (
<motion.div
layout
onClick={() => setIsOpen(!isOpen)}
>
<motion.h3 layout>
{header}
// ...
</motion.h3>
{isOpen ? (<motion.div layout>{content}</motion.div>) : null}
</motion.div>
);
}
import { LayoutGroup } from 'motion/react';
return (
<LayoutGroup>
{accordionData.map((item, index) => (
<AccordionItem
key={index}
header={item.header}
content={item.content}
/>
))}
</LayoutGroup>
);
}
Демонстрация:

LayoutGroup особенно полезен, когда:
У вас есть несколько независимых блоков (например, несколько аккордеонов или виджетов, раскиданных по странице), которые должны реагировать на изменения друг друга.
Вы используете общие анимации для разных элементов (
layoutId
).LayoutGroup
также незаменим для правильной работы свойстваlayoutId
.
layoutId: общие анимации элементов (Shared Layout Animations)
layoutId
нужен для создания крутых общих анимаций между разными элементами. Это когда один элемент плавно «перетекает» в другой, меняя при этом размер и положение. Представьте:
Подчеркивание активной вкладки, которое плавно едет за курсором.
Карточка товара, которая при клике плавно превращается в полноэкранный вид этого товара.
Элемент из одной части интерфейса, который «телепортируется» в другую с красивой анимацией

Поскольку layoutId
по умолчанию работает глобально (т.е. Motion считает, что любой элемент с одинаковым layoutId
— это один и тот же «объект», просто сменивший позицию), то LayoutGroup
позволяет сделать несколько таких независимых «перетеканий» на одной странице. Отдаём LayoutGroup
свой id, и тогда все layoutId
внутри этой группы будут работать только внутри неё, не конфликтуя с другими группами. Это как изолировать разные анимационные сценарии, чтобы они не мешали друг другу.
// ...
function Tab({ label, icon, isSelected, onClick }) {
return (
<motion.div
style={{
color: isSelected ? '#2196f3' : '#666'
}}
onClick={onClick}
>
// ...
{isSelected
? (<motion.div layoutId="underline" />)
: null}
</motion.div>
)
}
function TabRow({ items, selectedTab, onTabSelect }) {
return (
<LayoutGroup id="tabs">
{items.map(item => (
<Tab
key={item.id}
label={item.label}
icon={item.icon}
isSelected={selectedTab === item.id}
onClick={() => onTabSelect(item.id)}
/>
))}
</LayoutGroup>
)
}
// ...
Motion Values
Когда мы пробуем делать анимацию в React через useState
, всё быстро упирается в производительность: каждое обновление значения тянет за собой ре-рендер компонента. Для плавных 60 fps это означает десятки ненужных обновлений в секунду, что тяжело для приложения.
Здесь и появляется Motion Value
— особый объект, который хранит анимационные данные и напрямую обновляет DOM, минуя стандартный цикл React. По сути, это «живое» значение, которое меняется непрерывно и при этом не заставляет компонент лишний раз перерисовываться. Благодаря этому Motion Value
становится базой для любых плавных и сложных анимаций без просадок по производительности.
Работать с Motion Value
вручную можно через методы set()
, get()
и подписки на события on()
. Но чаще всего используются хуки, которые позволяют создавать, связывать и трансформировать такие значения декларативно.
Ключевые хуки Motion Value
:
Название хука |
Что делает |
Зачем нужен |
|
Создает базовое Motion Value. |
Для значений, которые управляются вручную или при перетаскивании. |
|
Преобразует одно или несколько Motion Value в новое. |
Для связывания данных (например, прокрутка) с визуальным свойством (например, масштаб). |
|
Анимирует значение с пружинной физикой. |
Для создания плавных, естественных переходов и инерции. |
|
Отслеживает скорость изменения значения. |
Для анимаций, которые реагируют на скорость движения. |
|
Создает динамические CSS-строки. |
Для динамического формирования сложных CSS-свойств, таких как |
|
Возвращает Motion Value, обновляющееся один раз за кадр. |
Для непрерывных, зацикленных анимаций (например, вращение). |
|
Слушает события Motion Value. |
Выполнить что-то, когда анимация началась/закончилась. |
Оставлю ссылку на демонстрацию примеров из таблицы.
Drag&Drop
Самый простой способ сделать компонент motion перетаскиваемым — добавить свойство drag
. Например, drag={true}
— это свободное перемещение, drag="x"
ограничит движение по горизонтали, а если задатьdragConstraints
, то элемент не будет вылетать за пределы.

Для создания интуитивно понятных списков с возможностью перетаскивания (привет, TODO-лист!) motion предлагает специальные компоненты Reorder.
Reorder.Group: оборачивает весь ваш список. Ему нужны values (массив данных) и
onReorder
(функция, которая обновит ваш state с новым порядком).Reorder.Item
: рендерится для каждого элемента списка. Ему нужны value (значение элемента) и, конечно же, уникальныйkey
.Reorder.Item
уже настроены на анимации макета, так что соседние элементы будут плавно сдвигаться, когда вы что-то перетаскиваете. Они также дружат сAnimatePresence
для плавного появления/исчезновения.
Важное замечание: если ваш список находится внутри прокручиваемого контейнера, добавьте layoutScroll
к Reorder.Group, чтобы всё работало как часы.
Иногда нужно начать перетаскивание с другого элемента. Используйте хук useDragControls
. Создаём контроллеры, передаём их motion-компоненту и вызываем controls.start(event)
с элемента-триггера. Не забудьте touch-action: none
для сенсорных устройств!
import { motion, useDragControls } from "motion/react"
function Example() {
const controls = useDragControls()
return (
<motion.div drag dragControls={controls} dragListener={false}>
<button
onPointerDown={(e) => controls.start(e)}
>
Жми и тяни!
</button>
</motion.div>
)
}
UseScroll и useInView — для анимаций, связанных с прокруткой
Анимации, связанные с прокруткой, — это тренд.
Хук useScroll
— главный инструмент для отслеживания позиции прокрутки. Он возвращает четыре Motion Values:
scrollX
, scrollY
(абсолютные позиции), scrollXProgress
и scrollYProgress
(нормализованные значения от 0 до 1, показывающие прогресс прокрутки).
Прокрутка страницы: по умолчанию отслеживает весь браузер. Идеально для глобальных индикаторов прогресса.
Прокрутка элемента: передайте
ref
элемента вcontainer
, чтобы отслеживать прокрутку внутри него (например, карусели).Позиция элемента внутри контейнера: используйте
target
сref
элемента, чтобы отслеживать его прогресс внутри прокручиваемого контейнера (например, элементы, появляющиеся в длинном списке).
Опция offset
: точное определение триггеров анимации.
Это самая мощная, но поначалу запутанная опция для useScroll
. offset
позволяет точно определить, когда начинается и заканчивается анимация прогресса прокрутки, указывая точки пересечения между target
и container
. Можно использовать ключевые слова ("start", "center", "end"), числа (0-1), пиксели ("100px") или проценты ("50%"). Например, "start end" означает, что анимация начинается, когда верхний край вашего элемента встречается с нижним краем области просмотра.
Таблица: Опции offset
для useScroll
.
Значение offset |
Что это значит |
["start start", "end end"] |
По умолчанию. Анимация от начала элемента до его конца в контейнере. |
["start end", "end start"] |
Анимация от момента, когда элемент появляется снизу, до момента, когда он исчезает сверху. |
["0 0.5", "1 0.5"] |
Анимация от начала элемента, когда он в середине контейнера, до его конца, когда он в середине контейнера. |
Демонстрация примеров из таблицы.
Хук useInView
— это легкий (всего 0.6 КБ!) хук, который определяет, когда элемент входит в область просмотра или покидает её. Он возвращает true
, если элемент виден, и false
в противном случае.
Использование: Создайте
ref
, передайте его вuseInView
, а затем прикрепитеref
к элементу, который хотите отслеживать.Опции:
root
(для отслеживания внутри конкретного контейнера),margin
(добавляет отступы к области обнаружения),once
(сработает только один раз),amount
(сколько элемента должно быть видно: "some", "all" или число от 0 до 1).
import { useRef, useEffect } from "react";
import { useInView } from "motion/react";
export function InViewBasic() {
const ref = useRef<HTMLDivElement>(null);
const isInView = useInView(ref, { once: true });
useEffect(() => {
if (isInView) {
// запускать побочные эффекты только когда элемент в зоне видимости
}
}, [isInView]);
return <div ref={ref} />;
}
useInView
— это не просто детектор видимости, это оптимизированный инструмент для запуска анимаций только тогда, когда они действительно нужны, плюс экономит ресурсы.
MotionConfig и useReducedMotion — для единообразия и контроля анимации на уровне всего приложения
Оберните им часть вашего приложения, и можно установить глобальные настройки, которые будут применяться ко всем дочерним motion-компонентам.
Ключевые свойства MotionConfig
:
transition
: определяет переход по умолчанию для всех дочерних компонентов, если они не указали свой собственный.reducedMotion
: это свойство позволяет установить политику обработки уменьшенного движения для всего сайта. По умолчанию"never"
. Можно установить"user"
(учитывает настройки пользователя),"always"
(всегда уменьшенное движение) или"never"
(игнорирует). Когда уменьшенное движение активно, анимации трансформации и макета отключаются, а другие (например,opacity
) продолжают работать.
MotionConfig
— центральный пульт управления анимациями в большом приложении. Позволяет поддерживать единый стиль и поведение.
Хук useReducedMotion
— более тонкий контроль на уровне компонента. Возвращает true
, если на устройстве пользователя включена настройка «Уменьшенное движение», и активно реагирует на её изменения.
Это позволяет адаптировать UI под предпочтения пользователя. Например, заменить насыщенные анимации на более простые, отключить автовоспроизведение видео или параллакс.
Заключение
Библиотека Motion — мощный и интуитивно понятный способ добавления сложных и производительных анимаций в React-приложения. Благодаря декларативному подходу, умному управлению DOM и обширному набору хуков и компонентов, она значительно упрощает создание интерактивного и динамичного пользовательского интерфейса. Надеюсь, эта статья поможет вам уверенно начать работу с Motion и сэкономить время на пути к созданию потрясающих анимаций.
Источники: