Core Concepts
Named timelines, from/to resolution, implicit values, and how the SDK works internally.
The SDK is built around a small set of concepts. Understand these and everything else follows naturally.
Named Timelines
Every animation is a named timeline. The name is a registry key — calling Motion('hero') always returns the same Timeline instance, no matter where in your code you call it.
import { Motion } from "@motion.page/sdk";
// Create a timeline named "hero"
Motion("hero", "#hero", {
from: { opacity: 0, y: 40 },
duration: 0.8,
ease: "power2.out",
}).onPageLoad();
// Later — retrieve the same instance by name
const tl = Motion("hero");
tl.pause(); The registry exposes three static helpers for inspecting registered timelines:
| Method | Returns | Description |
|---|---|---|
Motion.get(name) | Timeline | undefined | Get a timeline by name, or undefined if it doesn’t exist |
Motion.has(name) | boolean | Check whether a named timeline is registered |
Motion.getNames() | string[] | Array of all currently registered timeline names |
From/To Resolution
The SDK auto-resolves the missing endpoint from the element’s current computed CSS. You rarely need to specify both from and to.
| Config | Behavior | Use Case |
|---|---|---|
from only | SDK reads current CSS as to | Reveal animations — start hidden/offset, animate into natural state |
to only | SDK reads current CSS as from | Hover/exit effects — start at natural state, animate to custom values |
| Both | Both endpoints explicit | When neither endpoint matches the element’s natural CSS |
// Reveal — start invisible and shifted, land on the element's natural CSS
Motion("reveal", ".card", {
from: { opacity: 0, y: 50 },
duration: 0.6,
ease: "power2.out",
}).onPageLoad(); // Hover lift — start at natural state, animate to raised state on hover
Motion("card-hover", ".card", {
to: { y: -8, boxShadow: "0 12px 24px rgba(0,0,0,0.15)" },
duration: 0.3,
ease: "power2.out",
}).onHover({ each: true, onLeave: "reverse" }); // Parallax — neither endpoint matches the element's natural position
Motion("parallax", ".panel", {
from: { x: -50 },
to: { x: 50 },
duration: 1,
}).onScroll({ scrub: true }); Natural CSS Defaults
The SDK treats the following values as an element’s natural state. When they match what you want as the endpoint, omit them — the SDK fills them in automatically from the computed CSS.
opacity: 1 · x: 0 · y: 0 · z: 0 · scale: 1 · scaleX: 1 · scaleY: 1 · rotate: 0
// ✅ Correct — from-only; SDK reads opacity:1, y:0 from the element's CSS
Motion("fade-up", ".hero", {
from: { opacity: 0, y: 50 },
duration: 0.8,
}).onPageLoad();
// ❌ Redundant — opacity:1 and y:0 are natural defaults, the to: block adds nothing
Motion("fade-up", ".hero", {
from: { opacity: 0, y: 50 },
to: { opacity: 1, y: 0 },
duration: 0.8,
}).onPageLoad(); Set Mode
Motion.set(target, vars) immediately applies properties with zero duration — no animation, no timeline. Use it to initialise element states before an animation runs, or to snap elements back to a specific state.
// Hide elements before the page loads so there's no flash of unstyled content
Motion.set(".card", { opacity: 0, y: 30 });
// Then reveal them on scroll
Motion("cards", ".card", {
from: { opacity: 0, y: 30 },
duration: 0.6,
stagger: 0.08,
}).onScroll({ scrub: false, toggleActions: "play none none none" }); Three Overloads
Motion has three call signatures:
// 1. Retrieve an existing timeline by name
const tl = Motion("hero");
// 2. Single-animation timeline — one target, one config object
Motion("fade", "#banner", {
from: { opacity: 0 },
duration: 0.6,
}).onPageLoad();
// 3. Multi-step timeline — array of animation entries
Motion("sequence", [
{ target: "#title", from: { opacity: 0, y: 30 }, duration: 0.5 },
{ target: "#subtitle", from: { opacity: 0, y: 20 }, duration: 0.4, position: "-=0.2" },
{ target: "#cta", from: { opacity: 0, y: 20 }, duration: 0.4, position: "-=0.1" },
]).onPageLoad(); Each entry in the multi-step array is an AnimationEntry — the same AnimationConfig shape extended with a target field and an optional position for timeline placement.
The TargetInput Type
The target parameter (overload 2) and the target field in each entry (overload 3) accept a TargetInput:
| Type | Example |
|---|---|
| CSS selector string | "#hero", ".card", "[data-animate]" |
string[] | ["#title", "#subtitle"] |
Element | document.querySelector(".box") |
NodeList | document.querySelectorAll(".item") |
Element[] | Array of DOM elements |
| Plain object | { value: 0 } — for non-DOM tweening |
| Array of plain objects | [{ count: 0 }, { count: 100 }] |
CSS selectors are the most common form. Plain objects are useful for tweening arbitrary JS values — for example, driving a canvas draw or incrementing a counter:
const counter = { value: 0 };
Motion("count-up", counter, {
to: { value: 1000 },
duration: 2,
ease: "power2.out",
onUpdate: () => {
document.querySelector("#counter")!.textContent = Math.round(counter.value).toString();
},
}).onPageLoad(); How Triggers Chain
Timelines are inert until a trigger is chained. Creating a timeline with Motion(name, target, config) builds the animation but does not play or attach it to anything. You activate it by chaining a trigger method:
Motion("hero", "#hero", { from: { opacity: 0 }, duration: 0.8 })
.onPageLoad(); // ← trigger
Motion("cards", ".card", { from: { y: 40 }, duration: 0.5, stagger: 0.1 })
.onScroll({ scrub: false }); // ← trigger Available triggers:
| Method | Description |
|---|---|
.play() | Play immediately, right now |
.onPageLoad(config?) | Play when the DOM is ready; fires immediately if already loaded |
.onHover(config?) | Play on mouseenter, react on mouseleave |
.onClick(config?) | Toggle on click |
.onScroll(config?) | Scrub or snap animation to scroll position |
.onMouseMove(config?) | Drive animation progress from mouse position |
.onPageExit(config?) | Intercept link clicks, play animation, then navigate |
.onGesture(config) | Map pointer/touch/wheel/scroll gestures to animation progress |
.onCursor(config) | Replace the native cursor with an animated custom cursor |
Each trigger has its own configuration options — see the dedicated pages: Scroll Trigger, Hover, Click, Mouse Movement, Page Load, Page Exit, Observer Gesture, Custom Cursor.
Appending Warning
Calling Motion(name, target, config) when name already exists in the registry appends new entries to that timeline — it does not replace it. This is intentional for multi-step sequences built across multiple calls, but it will surprise you if you’re trying to rebuild a timeline in response to a state change.
Call .kill() first to destroy the existing instance before recreating it:
// ❌ Wrong — appends a second animation to the existing "hero" timeline
Motion("hero", "#hero", { from: { opacity: 0 }, duration: 0.8 }).onPageLoad();
// ✅ Correct — kill first, then rebuild from scratch
Motion("hero").kill();
Motion("hero", "#hero", { from: { opacity: 0 }, duration: 0.8 }).onPageLoad(); Motion.killAll() destroys every registered timeline at once — useful for full teardown before reinitialising (e.g. on SPA navigation).