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.

typescript
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:

MethodReturnsDescription
Motion.get(name)Timeline | undefinedGet a timeline by name, or undefined if it doesn’t exist
Motion.has(name)booleanCheck 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.

ConfigBehaviorUse Case
from onlySDK reads current CSS as toReveal animations — start hidden/offset, animate into natural state
to onlySDK reads current CSS as fromHover/exit effects — start at natural state, animate to custom values
BothBoth endpoints explicitWhen neither endpoint matches the element’s natural CSS
from-only.ts
// 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();
to-only.ts
// 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" });
both.ts
// 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

typescript
// ✅ 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.

typescript
// 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:

overloads.ts
// 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:

TypeExample
CSS selector string"#hero", ".card", "[data-animate]"
string[]["#title", "#subtitle"]
Elementdocument.querySelector(".box")
NodeListdocument.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:

typescript
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:

typescript
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:

MethodDescription
.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:

rebuild.ts
// ❌ 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).