Переход с Framer Motion

Framer Motion построен вокруг React-абстракций и декларативных компонентов. Анимации описываются через motion.div, variants, animate, initial, exit, а жизненный цикл синхронизируется с рендером React. Вся система тесно связана с виртуальным DOM и паттернами React.

Motion One опирается на Web Animations API и императивный API уровня DOM. Основной механизм — функция animate(), работающая напрямую с DOM-элементами или CSS-селекторами. Отсутствует слой React-обёрток, что делает библиотеку универсальной для любых фронтенд-стеков.

Разница в подходе формирует фундамент миграции:

  • декларативная модель Framer Motion заменяется вызовами функций
  • variants заменяются объектами ключевых кадров или последовательностями
  • управление жизненным циклом переносится в код приложения
  • анимации становятся независимыми от React-рендера

Модель анимации и её переосмысление

Framer Motion использует концепцию variants, где состояния описываются как именованные наборы стилей:

const variants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 }
}

Motion One заменяет эту модель на явное описание ключевых кадров:

import { animate } from "motion"

animate(
  element,
  { opacity: [0, 1], transform: ["translateY(20px)", "translateY(0px)"] },
  { duration: 0.4, easing: "ease-out" }
)

Семантика смещается от «состояний» к «интерполяции значений».

Ключевой момент миграции — переход от описания состояний UI к описанию изменения значений во времени.


Замена motion-компонентов

В Framer Motion базовая единица — компонент:

<motion.div
  initial={{ opacity: 0 }}
  animate={{ opacity: 1 }}
/>

В Motion One отсутствует JSX-слой. Эквивалент строится через ссылку на DOM:

import { animate } from "motion"

const el = document.querySelector(".box")

animate(el, {
  opacity: [0, 1]
})

В React-проектах DOM-элемент обычно получают через ref:

import { useEffect, useRef } from "react"
import { animate } from "motion"

function Box() {
  const ref = useRef(null)

  useEffect(() => {
    animate(ref.current, {
      opacity: [0, 1],
      transform: ["scale(0.9)", "scale(1)"]
    })
  }, [])

  return <div ref={ref} />
}

Таким образом исчезает слой motion.div, а управление анимацией переходит в эффекты жизненного цикла.


Переход от variants к композиции анимаций

Framer Motion активно использует каскадные variants:

const list = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.1
    }
  }
}

В Motion One аналогичная логика реализуется через последовательности и таймлайны.

import { stagger, animate } from "motion"

const items = document.querySelectorAll(".item")

animate(
  items,
  { opacity: [0, 1], y: [10, 0] },
  { delay: stagger(0.1) }
)

Система stagger становится функцией, а не частью декларативного объекта состояния.

Для сложных сценариев используется timeline:

import { timeline } from "motion"

timeline([
  [".title", { opacity: [0, 1] }, { duration: 0.3 }],
  [".subtitle", { opacity: [0, 1], y: [10, 0] }, { at: "-0.1" }],
  [".button", { scale: [0.8, 1] }, { at: 0.2 }]
])

Таймлайн заменяет систему вложенных variants и родительских контроллеров.


Анимации появления и исчезновения

В Framer Motion используется AnimatePresence:

<AnimatePresence>
  {show && (
    <motion.div exit={{ opacity: 0 }} />
  )}
</AnimatePresence>

В Motion One отсутствует виртуальный слой управления удалением DOM. Анимация выхода реализуется вручную:

import { animate } from "motion"

if (!show) {
  animate(
    el,
    { opacity: [1, 0] },
    {
      duration: 0.2,
      onComplete: () => el.remove()
    }
  )
}

Контроль жизненного цикла становится явным этапом логики приложения.


Drag и жесты

Framer Motion предоставляет встроенные абстракции drag, whileHover, whileTap.

Motion One разделяет поведение на отдельные модули и ручную обработку событий DOM:

let isDragging = false

el.addEventListener("pointerdown", () => {
  isDragging = true
})

el.addEventListener("pointermove", (e) => {
  if (!isDragging) return
  el.style.transform = `translate(${e.clientX}px, ${e.clientY}px)`
})

el.addEventListener("pointerup", () => {
  isDragging = false
})

Для анимации возврата используется animate():

animate(el, {
  transform: ["translate(300px, 200px)", "translate(0px, 0px)"]
}, { duration: 0.4 })

Scroll-анимации

Framer Motion использует useScroll и useTransform:

const { scrollYProgress } = useScroll()

В Motion One используется scroll API:

import { scroll } from "motion"

scroll(({ y }) => {
  el.style.transform = `translateY(${y * 100}px)`
})

Также возможна привязка прогресса к анимации:

scroll(animate(".progress", {
  scaleX: [0, 1]
}))

Управление синхронизацией и композиция

Framer Motion управляет синхронизацией через React state и props.

Motion One использует явную композицию анимационных задач:

  • последовательности через timeline
  • параллельные анимации через массивы целей
  • синхронизацию через at, delay, stagger

Пример параллельного запуска:

animate(".a", { opacity: [0, 1] })
animate(".b", { y: [20, 0] })

Пример синхронизированного запуска:

timeline([
  [".a", { opacity: [0, 1] }],
  [".b", { opacity: [0, 1] }, { at: 0 }]
])

Замена useAnimationControls

Framer Motion предоставляет imperative API:

controls.start({ opacity: 1 })

В Motion One управление осуществляется через сохранение экземпляра анимации:

import { animate } from "motion"

const animation = animate(el, { opacity: [0, 1] })

animation.pause()
animation.play()
animation.stop()

Контроллер становится объектом результата, а не отдельной абстракцией.


Переход от CSS-подобных объектов к массивам значений

Framer Motion активно использует объектный стиль:

{ opacity: 1, y: 20 }

Motion One использует массивы для интерполяции:

{ opacity: [0, 1], transform: ["translateY(20px)", "translateY(0px)"] }

Это фундаментальное изменение:

  • объект = статическое состояние
  • массив = траектория изменения

Производительность и модель выполнения

Framer Motion работает через React reconciliation и собственный scheduler. Каждое обновление состояния может приводить к пересчёту анимационных пропсов.

Motion One использует Web Animations API, что даёт:

  • выполнение анимаций на уровне браузера
  • минимальное участие JS после запуска
  • снижение нагрузки на main thread
  • более предсказуемую производительность при большом количестве анимаций

Миграция часто сопровождается переносом логики из React в DOM-слой, что уменьшает количество ререндеров.


Структурные паттерны переноса логики

Типичная замена архитектуры выглядит следующим образом:

Framer Motion:

  • UI = функция состояния
  • анимации = декларация в JSX
  • управление = props + hooks

Motion One:

  • UI = DOM
  • анимации = функции
  • управление = события и эффекты

Пример переноса компонента карточки:

<motion.div
  whileHover={{ scale: 1.05 }}
  animate={{ opacity: 1 }}
/>

Эквивалент:

el.addEventListener("pointerenter", () => {
  animate(el, { transform: ["scale(1)", "scale(1.05)"] })
})

el.addEventListener("pointerleave", () => {
  animate(el, { transform: ["scale(1.05)", "scale(1)"] })
})

Смена философии разработки интерфейсов

Переход от Framer Motion к Motion One означает смещение от декларативного UI к процедурному управлению анимацией.

Вместо описания «что должно быть» формируется последовательность «что происходит во времени»:

  • состояния заменяются переходами
  • компоненты заменяются DOM-селекторами
  • props заменяются событиями
  • lifecycle React заменяется эффектами и callbacks

Такой переход особенно заметен в сложных интерфейсах с большим количеством синхронных анимаций, где Motion One обеспечивает более низкоуровневый и контролируемый слой управления движением элементов.