При масштабировании интерфейсов на основе анимаций быстро возникает
повторяемость: одинаковые переходы появления, схожие движения карточек,
однотипные эффекты наведения, идентичные последовательности для списков.
Прямое копирование конфигураций приводит к дублированию параметров
duration, easing, delay, а также
к расхождению поведения при последующих изменениях.
Motion One предоставляет набор примитивов, которые позволяют
выстраивать уровень абстракции поверх низкоуровневых вызовов
animate(), timeline() и inView(),
формируя повторно используемые паттерны поведения вместо разрозненных
деклараций.
В основе большинства анимаций лежит объект параметров. Даже простая анимация появления уже содержит набор повторяемых характеристик:
Типичный фрагмент без абстракции:
import { animate } from "motion"
animate(
".card",
{ opacity: [0, 1], transform: ["translateY(10px)", "translateY(0px)"] },
{ duration: 0.4, easing: "ease-out" }
)
При увеличении числа компонентов подобные конфигурации начинают повторяться. Абстракция вводится через выделение общего профиля анимации.
Повторяющиеся параметры группируются в именованные профили. Такой профиль становится базовой единицей повторного использования.
const fadeUp = {
keyframes: {
opacity: [0, 1],
transform: ["translateY(12px)", "translateY(0px)"]
},
options: {
duration: 0.45,
easing: "ease-out"
}
}
Теперь анимации перестают описываться полностью каждый раз, а используют готовый профиль:
import { animate } from "motion"
animate(".card", fadeUp.keyframes, fadeUp.options)
Подобный подход устраняет расхождение в ощущении интерфейса, так как единая логика движения становится централизованной.
Профили недостаточны, когда требуется вариативность: разные задержки, масштабы, направления движения. В этом случае вводится уровень фабрик.
Фабрика анимации возвращает конфигурацию на основе входных параметров:
const createFadeUp = (distance = 12, duration = 0.45) => ({
keyframes: {
opacity: [0, 1],
transform: [`translateY(${distance}px)`, "translateY(0px)"]
},
options: {
duration,
easing: "ease-out"
}
})
Теперь поведение перестает быть фиксированным и превращается в параметризованную модель.
animate(".card", createFadeUp(20).keyframes, createFadeUp(20).options)
Фабрики позволяют строить семейства анимаций, где различия контролируются числовыми или логическими параметрами, а не копированием кода.
Сложные интерфейсы редко состоят из одного движения. Чаще используется комбинация эффектов: появление, смещение, масштабирование, синхронные задержки.
Motion One позволяет выстраивать композицию через последовательное или параллельное выполнение анимаций, но архитектурно важнее создать слой композиции на уровне описаний.
const fade = {
opacity: [0, 1]
}
const slideUp = (y = 10) => ({
transform: [`translateY(${y}px)`, "translateY(0px)"]
})
const composed = (y) => ({
keyframes: {
...fade,
...slideUp(y)
},
options: {
duration: 0.5,
easing: "ease-out"
}
})
Здесь происходит не просто объединение эффектов, а формирование композиционного паттерна, где базовые движения становятся строительными блоками.
При росте числа анимаций возникает необходимость в каскадной системе настроек. Базовый пресет задаёт общие параметры, поверх которых строятся специализированные варианты.
const base = {
duration: 0.4,
easing: "ease-out"
}
const fadeUp = (distance = 10) => ({
keyframes: {
opacity: [0, 1],
transform: [`translateY(${distance}px)`, "translateY(0px)"]
},
options: base
})
В этом подходе базовый объект становится аналогом дизайн-системы для движения, где любые изменения касаются всех производных анимаций одновременно.
Одним из наиболее повторяемых паттернов являются stagger-анимации
списков. Вместо ручного указания delay для каждого элемента
вводится вычисляемая модель.
const stagger = (index, step = 0.05) => index * step
Применение:
import { animate } from "motion"
document.querySelectorAll(".item").forEach((el, i) => {
animate(
el,
{ opacity: [0, 1], transform: ["translateY(8px)", "translateY(0px)"] },
{ delay: stagger(i) }
)
})
Абстракция превращает линейную последовательность в формализованное правило, где поведение определяется индексом, а не ручной настройкой.
Motion One предоставляет timeline() как инструмент
координации сложных сцен. Повторяемость проявляется в типовых сценариях:
появление блока, затем заголовка, затем контента.
import { timeline } from "motion"
const createRevealTimeline = (container) => {
const el = container.querySelectorAll(".part")
return timeline([
[el[0], { opacity: [0, 1] }, { duration: 0.3 }],
[el[1], { opacity: [0, 1], transform: ["translateY(10px)", "translateY(0px)"] }, { at: "-0.1", duration: 0.4 }],
[el[2], { opacity: [0, 1] }, { at: "-0.2", duration: 0.5 }]
])
}
Фабрика timeline превращает последовательность действий в повторяемую структуру, которую можно применять к разным контейнерам без переписывания логики.
Вместо описания анимации как объекта всё чаще используется модель «поведение элемента», где анимация является функцией состояния.
const applyHoverMotion = (el) => {
el.addEventListener("mouseenter", () => {
animate(el, { scale: 1.05 }, { duration: 0.2 })
})
el.addEventListener("mouseleave", () => {
animate(el, { scale: 1 }, { duration: 0.2 })
})
}
Такая абстракция переводит анимации из декларативного слоя в поведенческий, что особенно полезно при повторяющихся интеракциях.
При множестве UI-состояний (open, close, active, idle) возникает необходимость унифицировать переходы.
const states = {
hidden: { opacity: 0, transform: "translateY(8px)" },
visible: { opacity: 1, transform: "translateY(0px)" },
exit: { opacity: 0, transform: "translateY(-8px)" }
}
Применение:
animate(".panel", states.hidden, { duration: 0 })
animate(".panel", states.visible, { duration: 0.4, easing: "ease-out" })
Словарь состояний устраняет дублирование промежуточных значений и делает переходы между состояниями системными.
Motion One тесно интегрируется с CSS, что позволяет переносить повторяющиеся параметры в переменные.
:root {
--motion-duration: 0.4s;
--motion-easing: ease-out;
}
animate(
".box",
{ opacity: [0, 1] },
{
duration: "var(--motion-duration)",
easing: "var(--motion-easing)"
}
)
Таким образом, анимационные паттерны становятся частью общей системы стилей, а не изолированным JavaScript-слоем.
Списки элементов часто требуют одинакового поведения. Генерация избавляет от повторяющегося обхода DOM.
const animateList = (selector, animationFactory) => {
document.querySelectorAll(selector).forEach((el, i) => {
const config = animationFactory(i, el)
animate(el, config.keyframes, config.options)
})
}
Использование:
animateList(".card", (i) => ({
keyframes: { opacity: [0, 1], transform: ["translateY(10px)", "translateY(0px)"] },
options: { delay: i * 0.05, duration: 0.4 }
}))
Функция становится универсальным адаптером между DOM-структурой и анимационной логикой.
На уровне архитектуры выделяется переход от «как анимировать» к «что происходит». Вместо описания параметров создаются доменные функции:
const enterCard = (el) => animate(el, { opacity: [0, 1], y: [10, 0] }, { duration: 0.4 })
const exitCard = (el) => animate(el, { opacity: [1, 0], y: [0, -10] }, { duration: 0.3 })
Дальнейшая работа с интерфейсом оперирует уже смысловыми действиями, а не техническими характеристиками.
Абстракция повторяющихся паттернов формируется не одним механизмом, а совокупностью слоёв:
Каждый слой уменьшает количество повторяющегося кода и увеличивает уровень выражения логики движения, смещая её от технических деталей к структурным правилам поведения интерфейса