Motion() Factory
Complete reference for the Motion() function — retrieve, create, and configure timelines.
Motion() is the single entry point for every animation in the SDK. Call it to create a named timeline, retrieve an existing one, or build a multi-step sequence — then chain a trigger to activate it.
Import
import { Motion } from "@motion.page/sdk";No-bundler environments can import Motion directly from the CDN:
<script type="module">
import { Motion } from "https://cloud.motion.page/sdk/latest.js";
Motion("fade", ".box", { from: { opacity: 0 }, duration: 0.6 }).onPageLoad();
</script>Function Signatures
Motion has three call signatures:
// 1. Retrieve an existing timeline by name
Motion(name: string): Timeline
// 2. Single-animation timeline — one target, one config object
Motion(name: string, target: TargetInput, config: AnimationConfig): Timeline
// 3. Multi-step timeline — array of animation entries
Motion(name: string, animations: AnimationEntry[]): TimelineThe name is a registry key. The same name always returns the same Timeline instance, anywhere in your code. Passing target and config with an already-registered name appends to that timeline rather than replacing it — see Appending Warning.
Basic Usage
import { Motion } from "@motion.page/sdk";
// Create and play immediately
Motion("hero", "#hero", {
from: { opacity: 0, y: 40 },
duration: 0.8,
ease: "power2.out",
}).play();
// Create and play on page load
Motion("cards", ".card", {
from: { opacity: 0, y: 30 },
duration: 0.6,
stagger: 0.08,
ease: "power2.out",
}).onPageLoad();
// Retrieve an existing timeline later by name
const tl = Motion("hero");
tl.pause();TargetInput
The target parameter (overload 2) and the target field in each AnimationEntry (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 counter or animating a custom data property:
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();AnimationConfig
The config object passed to overload 2, and embedded in each AnimationEntry in overload 3.
| Option | Type | Default | Description |
|---|---|---|---|
from |
AnimationVars |
— | Start values. SDK reads current CSS as to when only from is specified |
to |
AnimationVars |
— | End values. SDK reads current CSS as from when only to is specified |
duration |
number |
0.5 |
Animation duration in seconds |
delay |
number |
0 |
Seconds to wait before the animation starts |
ease |
string |
"power1.inOut" |
Easing curve — e.g. "power2.out", "elastic.out" |
stagger |
number | StaggerVars |
— | Per-element delay when target matches multiple elements |
repeat |
number | RepeatConfig |
— | Repeat count for this animation. -1 = infinite |
split |
SplitType |
— | Split text into chars, words, or lines before animating |
mask |
boolean |
false |
Wraps split elements in overflow: hidden containers for reveal effects |
flap |
FlapConfig |
— | Split-flap / character-scramble effect. Requires split: 'chars' |
fit |
FitConfig |
— | FLIP-style morph between two elements. Overrides from/to |
axis |
'x' | 'y' |
— | Axis binding for .onMouseMove() trigger |
onStart |
() => void |
— | Fired when the animation begins |
onUpdate |
(progress: number) => void |
— | Fired on every frame with current progress (0–1) |
onComplete |
() => void |
— | Fired when the animation finishes |
onRepeat |
(repeatCount: number) => void |
— | Fired after each repeat cycle |
onReverseComplete |
() => void |
— | Fired when animation completes in reverse |
Full TypeScript shape:
interface AnimationConfig {
from?: AnimationVars;
to?: AnimationVars;
duration?: number;
delay?: number;
ease?: string;
stagger?: number | StaggerVars;
repeat?: number | RepeatConfig;
split?: SplitType;
mask?: boolean;
flap?: FlapConfig;
fit?: FitConfig;
axis?: 'x' | 'y';
onStart?: () => void;
onUpdate?: (progress: number) => void;
onComplete?: () => void;
onRepeat?: (repeatCount: number) => void;
onReverseComplete?: () => void;
}AnimationVars
All properties that can appear in from and to blocks.
Transforms
| Property | Type | Notes |
|---|---|---|
x |
number | string |
Translate X — pixels by default (50 = 50px) |
y |
number | string |
Translate Y — pixels by default |
z |
number | string |
Translate Z — pixels by default |
rotate |
number | string |
Rotation in degrees (90 = 90deg) |
rotateX |
number | string |
3D rotation around X axis |
rotateY |
number | string |
3D rotation around Y axis |
rotateZ |
number | string |
3D rotation around Z axis (same as rotate) |
scale |
number |
Uniform scale — unitless (1.5 = 150%) |
scaleX |
number |
Horizontal scale |
scaleY |
number |
Vertical scale |
scaleZ |
number |
Depth scale |
skewX |
number | string |
Horizontal skew in degrees |
skewY |
number | string |
Vertical skew in degrees |
perspective |
number | string |
CSS perspective depth |
transformOrigin |
string |
Transform origin — e.g. "50% 50%", "top left", "0% 100%" |
Visual
| Property | Type | Notes |
|---|---|---|
opacity |
number |
0 (transparent) to 1 (fully visible) |
backgroundColor |
string |
Any CSS color string |
color |
string |
Text / foreground color |
filter |
string |
CSS filter string — e.g. "blur(4px) brightness(0.8)" |
clipPath |
string |
CSS clip-path shape function (camelCase form) |
'clip-path' |
string |
CSS clip-path shape function (kebab-case, must be quoted) |
boxShadow |
string |
CSS box-shadow string |
borderRadius |
number | string |
Border radius — px or string |
borderColor |
string |
Border color |
outlineColor |
string |
Outline color |
Layout
| Property | Type | Notes |
|---|---|---|
width |
number | string |
Element width — px or string with unit |
height |
number | string |
Element height — px or string with unit |
top |
number | string |
CSS top |
left |
number | string |
CSS left |
right |
number | string |
CSS right |
bottom |
number | string |
CSS bottom |
margin |
string |
Shorthand margin |
padding |
string |
Shorthand padding |
zIndex |
number |
Stacking order |
backgroundPosition |
string |
Background position |
Typography
| Property | Type | Notes |
|---|---|---|
fontSize |
number | string |
Font size |
lineHeight |
number | string |
Line height |
letterSpacing |
number | string |
Letter spacing |
SVG
| Property | Type | Notes |
|---|---|---|
fill |
string |
SVG fill color |
stroke |
string |
SVG stroke color |
drawSVG |
string | DrawSVGObject |
Animate SVG stroke — see drawSVG |
Motion Path
path: {
target: string | Element; // CSS selector, Element, or raw path data (M/m commands)
align?: string | Element; // Align bounding box to this element
alignAt?: [number, number]; // Origin point [x%, y%]; default [50, 50]
start?: number; // Path start fraction 0–1; default 0
end?: number; // Path end fraction 0–1; default 1
rotate?: boolean; // Auto-rotate along the path tangent
}CSS Custom Properties
Any CSS custom property — prefix with --:
to: { '--accent-color': '#9966FF', '--spacing': '24px' }from / to Resolution
The SDK auto-resolves the missing endpoint from the element’s computed CSS at build time. Specify only what’s different from the natural state.
| Config | Behavior |
|---|---|
from only |
SDK reads current CSS as to. Start at custom values, animate into natural state — use for reveals |
to only |
SDK reads current CSS as from. Start at natural state, animate to custom values — use for hover/exit effects |
| Both | Both endpoints explicit — use when neither endpoint matches the element’s natural CSS |
Natural CSS defaults — the SDK treats these as the element’s rest state. Omit them from to when they’re the target endpoint:
opacity: 1 · x: 0 · y: 0 · z: 0 · scale: 1 · scaleX: 1 · scaleY: 1 · rotate: 0
// ✅ from-only — SDK fills in opacity:1, y:0 from the element's CSS
Motion("reveal", ".card", {
from: { opacity: 0, y: 50 },
duration: 0.6,
}).onPageLoad();
// ✅ to-only — start at natural state, hover to raised state
Motion("lift", ".card", {
to: { y: -8, boxShadow: "0 12px 24px rgba(0,0,0,0.15)" },
duration: 0.3,
}).onHover({ each: true, onLeave: "reverse" });
// ✅ both — parallax where neither endpoint is the natural state
Motion("parallax", ".bg", {
from: { y: -60 },
to: { y: 60 },
}).onScroll({ scrub: true });
// ❌ redundant — opacity:1 and y:0 are natural defaults
Motion("bad", ".card", {
from: { opacity: 0, y: 50 },
to: { opacity: 1, y: 0 },
}).onPageLoad();Multi-Step Timelines
Pass an array of AnimationEntry objects as the second argument to build coordinated sequences across multiple targets.
Motion("intro", [
{ target: ".hero-title", from: { opacity: 0, y: 40 }, duration: 0.7, ease: "power2.out" },
{ target: ".hero-subtitle", from: { opacity: 0, y: 30 }, duration: 0.6, ease: "power2.out" },
{ target: ".hero-cta", from: { opacity: 0, y: 20 }, duration: 0.5, ease: "power2.out" },
]).onPageLoad();By default, each step starts immediately after the previous one ends. Control timing with the position field.
AnimationEntry
Every entry extends AnimationConfig with two additional fields:
| Field | Type | Description |
|---|---|---|
target |
TargetInput |
Required. CSS selector, Element, NodeList, or Element[] |
position |
number | string |
When in the timeline this step starts. Omit for sequential playback |
interface AnimationEntry extends AnimationConfig {
target: TargetInput;
position?: number | string;
}Position Syntax
Used in AnimationEntry.position, tl.call(), and scroll trigger end values.
| Value | Meaning |
|---|---|
0.5 |
Absolute — 0.5 s from the timeline start |
"+=0.5" |
0.5 s after the previous entry ends |
"-=0.3" |
0.3 s before the previous entry ends (overlap) |
">" |
Immediately after the previous entry ends (same as default) |
"<" |
Same start time as the previous entry (parallel) |
"<0.2" |
0.2 s after the start of the previous entry |
">-0.1" |
0.1 s before the end of the previous entry |
Motion("hero", [
// Title and image start at the same time (parallel)
{ target: ".title", from: { opacity: 0, y: 40 }, duration: 0.8, ease: "power3.out" },
{ target: ".image", from: { opacity: 0, scale: 0.9 }, duration: 0.8, ease: "power2.out", position: "<" },
// Badge appears 0.3 s after both start
{ target: ".badge", from: { opacity: 0, scale: 0 }, duration: 0.4, ease: "back.out", position: "<0.3" },
// Footer enters with a gap after everything else
{ target: ".footer", from: { opacity: 0 }, duration: 0.4, position: "+=0.2" },
]).onPageLoad();Timeline Object
Every Motion() call returns a Timeline instance with the following methods.
Playback
tl.play(from?: number): this // Play forward; optional start time in seconds
tl.pause(atTime?: number): this // Pause; optional snap-to time
tl.reverse(from?: number): this // Play backward; optional start time
tl.restart(): this // Seek to 0, restore initial CSS, play forward
tl.seek(position: number): this // Jump to time without playingState — Getter / Setter
Call with no argument to read; call with argument to set and return this for chaining.
tl.duration(): number
tl.progress(value?: number): number | this // Normalized 0–1
tl.time(value?: number): number | this // Current time in seconds
tl.timeScale(value?: number): number | this // Speed multiplier (2 = double speed)
tl.isActive(): boolean // True while playing
tl.getName(): string | undefined // Registered nameCallbacks at Positions
Insert callback functions at specific points in the timeline:
tl.call(callback: (...args: unknown[]) => void, params?: unknown[], position?: string | number): thisMotion("sequence", [
{ target: ".step-1", from: { opacity: 0 }, duration: 0.5 },
{ target: ".step-2", from: { opacity: 0 }, duration: 0.5 },
])
.call(() => console.log("halfway"), [], 0.5)
.call(() => console.log("done"))
.onPageLoad();Lifecycle Callbacks
tl.onStart(cb: () => void): this
tl.onUpdate(cb: (progress: number, time: number) => void): this
tl.onComplete(cb: () => void): this
tl.onRepeat(cb: (count: number) => void): thisCleanup
tl.kill(clearProps?: boolean): void // clearProps=true (default) restores initial CSS
tl.clear(): this // Reset and rebuild without destroying the instanceTimeline Repeat
.withRepeat() loops the entire timeline — all steps replay as a group. This is different from per-animation repeat, which loops only one individual animation.
tl.withRepeat(config: number | RepeatConfig): this// Infinite loop
Motion("pulse", ".dot", {
from: { scale: 0.8, opacity: 0.6 },
to: { scale: 1.2, opacity: 1 },
duration: 0.6,
}).withRepeat({ times: -1, yoyo: true }).onPageLoad();
// 3 times with yoyo + delay between cycles
Motion("bounce", [
{ target: ".box-a", to: { y: -40 }, duration: 0.4, ease: "power2.out" },
{ target: ".box-b", to: { y: -40 }, duration: 0.4, ease: "power2.out" },
]).withRepeat({ times: 3, yoyo: true, delay: 0.5 }).onPageLoad();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 building multi-step timelines across multiple calls, but it will surprise you when trying to rebuild a timeline in response to a state change.
// ❌ Wrong — appends a second animation to the existing "hero" timeline
Motion("hero", "#hero", { from: { opacity: 0 }, duration: 0.8 }).onPageLoad();
// (somewhere later)
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();Static Methods
| Method | Signature | Description |
|---|---|---|
Motion.set |
(target: TargetInput, vars: AnimationVars): void |
Zero-duration property apply. Processes through the full engine pipeline — values persist on DOM |
Motion.get |
(name: string): Timeline | undefined |
Get a timeline by name; undefined if not registered |
Motion.has |
(name: string): boolean |
Check whether a named timeline is registered |
Motion.getNames |
(): string[] |
Array of all currently registered timeline names |
Motion.kill |
(name: string): void |
Kill one timeline by name and restore initial CSS |
Motion.killAll |
(): void |
Kill every registered timeline |
Motion.reset |
(targets: TargetInput): void |
Kill animations, revert text splits, clear transform cache and inline styles |
Motion.refreshScrollTriggers |
(): void |
Recalculate all scroll trigger positions after layout changes |
Motion.cleanup |
(): void |
Detach active ScrollTriggers and safely remove their spacer/marker nodes; timelines remain registered |
Motion.context |
(fn: () => void): MotionContext |
Create a scoped context tracking all timelines created inside fn() |
Motion.utils |
MotionUtils |
Utility functions — see below |
Motion.set
Motion.set immediately applies CSS properties with no animation, no timeline. Use it to set initial element states before animations run, preventing flashes of unstyled content:
// Hide before page loads — no flash
Motion.set(".card", { opacity: 0, y: 30 });
// Reveal on scroll
Motion("cards", ".card", {
from: { opacity: 0, y: 30 },
duration: 0.6,
stagger: 0.08,
}).onScroll({ scrub: false, toggleActions: "play none none none" });Motion.context
Creates a scoped context that tracks all timelines created inside the callback. Returns a MotionContext object for clean teardown and reinitialisation — essential for SPA navigation, AJAX pagination, and React effects.
const ctx = Motion.context(() => {
Motion("hero", ".title", { from: { opacity: 0, y: 30 }, duration: 0.8 }).onPageLoad();
Motion("cards", ".card", { from: { y: 100 }, duration: 1 }).onScroll({ scrub: true });
});
// After dynamic content change (AJAX filter, pagination, etc.)
ctx.refresh(); // kills old animations, re-runs init, re-resolves selectors
// Add more animations to the context later
ctx.add(() => {
Motion("footer", ".footer-item", { from: { opacity: 0 }, duration: 0.4 }).onPageLoad();
});
// Full cleanup (React unmount, route change, etc.)
ctx.revert();React pattern:
useEffect(() => {
const ctx = Motion.context(() => {
Motion("fade", ".box", { from: { opacity: 0 }, duration: 0.6 }).onScroll({ scrub: true });
});
return () => ctx.revert();
}, []);Astro View Transitions:
let ctx: MotionContext | undefined;
document.addEventListener("astro:page-load", () => {
ctx?.revert();
ctx = Motion.context(() => {
Motion("page-reveal", ".animate-in", { from: { opacity: 0, y: 20 }, duration: 0.5 }).onPageLoad();
});
});Motion.utils
A collection of mathematical utilities. Also importable as a named export:
import { MotionUtils } from "@motion.page/sdk";| Method | Signature | Description |
|---|---|---|
toArray |
(target, scope?) → Element[] |
Convert selector / NodeList / HTMLCollection / Element to a flat array |
clamp |
(min, max, value?) → number | fn |
Clamp value within range. Curried when value is omitted |
random |
(min, max, snap?) → number |
Random number with optional snap increment |
snap |
(snapTo, value?) → number | fn |
Snap to nearest increment or array value. Curried when value is omitted |
interpolate |
(start, end, progress) → number |
Linear interpolation (lerp) between two values |
mapRange |
(inMin, inMax, outMin, outMax, value?) → number | fn |
Map a value from one range to another. Curried when value is omitted |
normalize |
(min, max, value?) → number | fn |
Normalize a value to 0–1. Curried when value is omitted |
wrap |
(min, max, value?) → number | fn |
Modular wrap within a range. Curried when value is omitted |
// toArray — convert selector to elements for manual iteration
const panels = Motion.utils.toArray(".panel");
// clamp — keep a value within bounds
const clamped = Motion.utils.clamp(0, 100, 150); // → 100
// Curried form — create a reusable clamp function
const clamp0to1 = Motion.utils.clamp(0, 1);
clamp0to1(1.5); // → 1
// mapRange — convert scroll progress to rotation
const progress = Motion.utils.mapRange(0, window.innerHeight, 0, 360, scrollY);
// interpolate — lerp between colors or numbers
const value = Motion.utils.interpolate(0, 100, 0.75); // → 75
// wrap — loop an index within a range
const index = Motion.utils.wrap(0, slides.length, currentIndex + 1);Trigger Methods
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.
| Method | Description |
|---|---|
.play() |
Play immediately, right now |
.onPageLoad(config?) |
Play when DOM is ready — fires immediately if already loaded |
.onHover(config?) |
Play on mouseenter, react on mouseleave |
.onClick(config?) |
Play/replay on click; optionally reverse or restart |
.onScroll(config?) |
Scrub or snap animation to scroll position |
.onMouseMove(config?) |
Drive 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: Page Load · Hover · Click · Scroll Trigger · Mouse Movement · Page Exit · Gesture · Custom Cursor.
each: true — Independent Per-Element Instances
Without each, all matched elements share one timeline instance (group behaviour). With each: true, every element gets its own independent timeline.
// Group — hovering any card animates all of them
Motion("cards", ".card", { to: { y: -8 }, duration: 0.3 }).onHover({ target: ".card", onLeave: "reverse" });
// Independent — each card animates on its own hover
Motion("cards", ".card", { to: { y: -8 }, duration: 0.3 }).onHover({ each: true, onLeave: "reverse" });each: true is supported in: .onHover(), .onClick(), .onScroll(), .onMouseMove(), .onGesture().
Stagger
stagger delays the start of each matched element’s animation, creating cascade effects.
Shorthand
Pass a number — each element waits that many seconds more than the previous:
Motion("list", ".item", {
from: { opacity: 0, y: 20 },
duration: 0.5,
stagger: 0.08,
}).onPageLoad();
// Element 1 at 0s, element 2 at 0.08s, element 3 at 0.16s …StaggerVars Object
| Option | Type | Default | Description |
|---|---|---|---|
each |
number |
— | Seconds between each element |
amount |
number |
— | Total spread across all elements (alternative to each) |
from |
'start' | 'end' | 'center' | 'edges' | 'random' | number |
'start' |
Which element starts first |
grid |
'auto' | [number, number] |
— | Enable 2D grid staggering |
axis |
'x' | 'y' |
— | Axis to measure distance for grid stagger |
ease |
string |
'none' |
Easing for the stagger delay distribution |
// Center-outward ripple on a grid
Motion("grid", ".cell", {
from: { opacity: 0, scale: 0.6 },
duration: 0.4,
stagger: { each: 0.04, from: "center", grid: "auto" },
ease: "power2.out",
}).onPageLoad();See Stagger for the full stagger reference.
Repeat Config
The repeat option on individual animations. For repeating the whole timeline, use .withRepeat().
// Shorthand number
repeat: 3 // 3 additional cycles after the first play
repeat: -1 // Infinite
// Full config object
interface RepeatConfig {
times: number; // Additional repetitions; -1 = infinite
delay?: number; // Seconds between each repetition
yoyo?: boolean; // Alternate direction each cycle
}// Infinite yoyo
Motion("breathe", ".logo", {
to: { scale: 1.08 },
duration: 1.2,
ease: "sine.inOut",
repeat: { times: -1, yoyo: true },
}).onPageLoad();
// 3 times with yoyo + rest delay
Motion("shake", ".error-field", {
from: { x: -6 },
to: { x: 6 },
duration: 0.06,
ease: "none",
repeat: { times: 7, yoyo: true, delay: 0 },
}).play();Text Splitter
Split a text element into individual spans before animating. Each part becomes independently animatable.
split values
| Value | Effect |
|---|---|
'chars' |
Wrap each character in a <span> |
'words' |
Wrap each word in a <span> |
'lines' |
Wrap each line in a <span> (layout-dependent) |
'chars,words' |
Both chars and words wrapped |
'words,lines' |
Both words and lines wrapped |
'chars,words,lines' |
All three levels wrapped |
Motion("headline", "h1", {
split: "words",
from: { opacity: 0, y: "110%" },
duration: 0.6,
stagger: { each: 0.06, from: "start" },
ease: "power3.out",
}).onPageLoad();Split elements receive data attributes for CSS targeting:
| Attribute | Set on | Index attribute |
|---|---|---|
[data-split-char] |
Each character span | data-char-index |
[data-split-word] |
Each word span | data-word-index |
[data-split-line] |
Each line span | data-line-index |
[data-split] |
Root element | — |
Inline elements (e.g. <span class="accent">) are preserved during splitting.
See Text Splitter for the full text splitter reference.
Mask
mask: true wraps each split element in an overflow: hidden container. The container clips the element during animation, creating a reveal effect where text appears to rise from beneath a surface.
Requires split to be set — mask has no effect without text splitting.
// Lines slide up from behind a clip — classic editorial reveal
Motion("reveal", ".tagline", {
split: "lines",
mask: true,
from: { y: "100%" },
duration: 0.7,
stagger: 0.1,
ease: "power3.out",
}).onPageLoad();The mask wrapper receives [data-split-mask] for CSS targeting.
Flap (Text Flapper)
A split-flap display effect that cycles through intermediate characters before landing on the real text. Requires split: 'chars' on the same animation.
Motion("scramble", ".tagline", {
split: "chars",
flap: {
type: "flip",
charset: "alphanumeric",
cycles: [2, 5],
},
from: { opacity: 0 },
duration: 1.2,
stagger: 0.04,
}).onPageLoad();FlapConfig
| Option | Type | Default | Description |
|---|---|---|---|
type |
FlapType |
— | Required. Visual effect for each cycle |
charset |
CharsetPreset | string |
'alphanumeric' |
Characters to cycle through. Pass a string for a custom set |
cycles |
number | [number, number] |
3 |
Intermediate characters before landing. Tuple = random range per cell |
perspective |
number |
400 |
CSS perspective depth in px — applies to flip and board only |
styledBoard |
boolean |
false |
Render decorative flip-board cell UI — applies to board only |
stableWidth |
boolean | 'cells' | 'container' |
true when styledBoard |
Layout-shift strategy. true/'cells' pins each char cell to the widest glyph; 'container' pins only the parent element’s outer width so letters flow naturally inside |
preserveWhitespaceCells |
boolean |
false |
Animate whitespace characters as cycling placeholder cells |
FlapType values
| Value | Description |
|---|---|
'flip' |
3D rotateX card flip. Composable with from/to transforms |
'fade' |
Opacity crossfade. Owns opacity — do not set opacity in from/to |
'slide' |
Characters slide in/out vertically |
'blur' |
Blur dissolve. Owns filter — do not set filter in from/to |
'scale' |
Characters scale in/out |
'board' |
Decorative split-flap board cells — use with styledBoard: true |
'none' |
No built-in style — run cycles only, drive visuals entirely via CSS |
CharsetPreset values
| Value | Characters |
|---|---|
'alphanumeric' |
A–Z, 0–9 |
'alpha' |
A–Z only |
'numeric' |
0–9 only |
'symbols' |
Punctuation and special characters |
'binary' |
0 and 1 only |
'hex' |
0–9, A–F |
'katakana' |
Japanese katakana characters |
'braille' |
Braille dot patterns |
See Text Flapper for the full reference.
Fit (FLIP Morphing)
Measure both elements’ bounding rects and animate the visual delta between them — a FLIP-style morph. When fit is set, from and to are ignored.
Motion("morph", ".source", {
fit: { target: ".destination", resize: true },
duration: 0.5,
ease: "power2.inOut",
}).play();FitConfig
| Option | Type | Default | Description |
|---|---|---|---|
target |
string |
— | Required. CSS selector for the destination element |
absolute |
boolean |
false |
Convert source to absolute positioning during animation |
scale |
boolean |
true |
Use scaleX/scaleY — GPU-accelerated but distorts text and borders |
resize |
boolean |
false |
Animate actual width/height — no distortion, triggers layout reflow. Mutually exclusive with scale |
See Fit Animation for the full reference.
drawSVG
Animate the visible portion of an SVG stroke. The element must have a stroke — the SDK auto-computes stroke-dasharray if not set.
| Input | Meaning |
|---|---|
"0% 100%" |
Full stroke visible |
"0% 0%" |
Fully hidden (start state for draw-in) |
"20% 80%" |
Middle portion only |
"50%" |
Shorthand for "0% 50%" |
"100px 500px" |
Pixel range along the stroke length |
"true" |
Full stroke (keyword) |
"false" / "none" |
Hidden |
{ start: 20, end: 80 } |
Object — values are percentages 0–100 (not 0–1) |
// Draw a path from left to right
Motion("draw", "path.line", {
from: { drawSVG: "0% 0%" },
to: { drawSVG: "0% 100%" },
duration: 1.2,
ease: "power2.inOut",
}).onScroll({ scrub: true });Object form:
{ start: 20, end: 80 }means 20% to 80% — values are percentages0–100, not fractions0–1.
See SVG Animations for the full drawSVG reference.
clip-path
Animate the CSS clip-path property between matching shape functions. Both clipPath (camelCase) and 'clip-path' (kebab-case, quoted) keys are accepted. The renderer writes both clip-path and -webkit-clip-path automatically.
| Shape | Example |
|---|---|
circle |
'circle(50% at 50% 50%)' |
ellipse |
'ellipse(60% 30% at 50% 50%)' |
inset |
'inset(10px 20px round 8px)' |
polygon |
'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)' |
rect |
'rect(10px 90px 90px 10px)' |
xywh |
'xywh(10% 10% 80% 80%)' |
Interpolation rules:
- Same shape on both ends for smooth interpolation. Cross-shape (e.g.
circle → inset) hard-swaps at progress ≥ 0.5. polygonvertex counts must match betweenfromandtofor smooth morphing.- Units are preserved (
%,px).
// Circular reveal — expand from center
Motion("reveal", "#hero", {
from: { 'clip-path': 'circle(0% at 50% 50%)' },
to: { 'clip-path': 'circle(75% at 50% 50%)' },
duration: 1,
ease: "power2.out",
}).onPageLoad();
// Diagonal wipe on hover
Motion("wipe", ".card-image", {
from: { clipPath: 'polygon(0% 0%, 100% 0%, 100% 100%, 0% 100%)' },
to: { clipPath: 'polygon(0% 0%, 0% 0%, 100% 100%, 0% 100%)' },
duration: 0.5,
ease: "power2.inOut",
}).onHover({ onLeave: "reverse" });See Clip Path for the full reference.
Easing Reference
The ease string follows the format "family.direction" — lowercase, e.g. "power2.out". Unknown strings fall back to "power1.out". Easing names are case-insensitive.
| Family | Variants | Best for |
|---|---|---|
power1 |
.in · .out · .inOut |
Subtle UI motion, understated transitions |
power2 |
.in · .out · .inOut |
Scroll reveals, card entrances — the everyday workhorse |
power3 |
.in · .out · .inOut |
Hero sections, modals, bold entrances |
power4 |
.in · .out · .inOut |
Cinematic moments, high-impact exits |
sine |
.in · .out · .inOut |
Delicate fades, floating loops |
expo |
.in · .out · .inOut |
Drawers, command palettes, snap-open UI |
circ |
.in · .out · .inOut |
Sorting animations, mechanical / precision UI |
back |
.in · .out · .inOut |
Playful cards, buttons — mild overshoot |
elastic |
.in · .out · .inOut |
Badges, tooltips, success states — spring energy |
bounce |
.in · .out · .inOut |
Game UI, mascots — discrete ball-drop bounces |
rough |
— | Glitch effects, error shakes, organic feel |
slow |
.in · .out · .inOut |
Dramatic pauses, attention-drawing loops |
none |
— | Scrub animations, progress bars — constant speed |
Omitting ease uses the SDK default: equivalent to "power1.inOut".
See Easing Functions for visual comparison and usage guidance.
Callbacks
Lifecycle callbacks can be defined inline in AnimationConfig or chained on the Timeline:
// Inline in config
Motion("reveal", ".box", {
from: { opacity: 0, y: 30 },
duration: 0.6,
onStart: () => console.log("started"),
onUpdate: (progress) => console.log(`progress: ${progress.toFixed(2)}`),
onComplete: () => console.log("done"),
onRepeat: (count) => console.log(`repeat #${count}`),
onReverseComplete: () => console.log("reversed"),
}).onPageLoad();
// Chained on the timeline
Motion("counter", "#count", {
from: { opacity: 0 },
duration: 0.4,
})
.onStart(() => startTracking())
.onUpdate((progress, time) => updateProgress(progress))
.onComplete(() => finishTracking())
.play();| Callback | Signature | Fires when |
|---|---|---|
onStart |
() => void |
Animation begins playing |
onUpdate |
(progress: number, time: number) => void |
Every animation frame |
onComplete |
() => void |
Animation reaches the end |
onRepeat |
(count: number) => void |
After each repeat cycle |
onReverseComplete |
() => void |
Animation completes while playing in reverse |
Dynamic Content & SPA
Use Motion.context() to scope animations for clean teardown and reinitialisation when DOM content changes — AJAX filters, pagination, SPA navigation, infinite scroll.
// Create a context — all timelines created inside are tracked
const ctx = Motion.context(() => {
Motion("hero", ".title", { from: { opacity: 0, y: 30 }, duration: 0.8 }).onPageLoad();
Motion("cards", ".card", { from: { y: 100 }, duration: 1 }).onScroll({ scrub: true });
});
// After dynamic content change (filter, pagination, etc.)
ctx.refresh(); // kills old animations, re-runs init, re-resolves all selectors
// Add more animations to the same context
ctx.add(() => {
Motion("new-section", ".new-item", { from: { opacity: 0 }, duration: 0.4 }).onPageLoad();
});
// Full cleanup — React unmount, route change, etc.
ctx.revert();Both published targets — the WordPress plugin and standalone / CDN exports — wrap your page’s animation code in Motion.context() automatically and expose MOTIONPAGE_FRONT.reinit(). Call it after AJAX or other dynamic content changes to silently re-initialise animations (including on newly added elements):
MOTIONPAGE_FRONT.reinit();See SPA Integration for framework-specific patterns (React, Vue, Next.js, Astro).
Anti-Patterns
| DON’T | DO instead |
|---|---|
from: { opacity: 0, y: 50 }, to: { opacity: 1, y: 0 } |
from: { opacity: 0, y: 50 } — opacity:1 and y:0 are natural defaults |
import { Motion } from '@motion/sdk' |
import { Motion } from '@motion.page/sdk' |
Call Motion("name", ...) twice expecting replacement |
Call Motion("name").kill() first, then rebuild |
Target multiple independent elements without each: true |
Add each: true to the trigger config |
querySelectorAll(".card").forEach(el => Motion(..., el, ...)) |
Motion("cards", ".card", {...}).onHover({ each: true }) |
| Mix external animation libraries with Motion SDK | Use Motion SDK exclusively |
drawSVG: { start: 0.2, end: 0.8 } expecting fractions |
Object values are percentages 0–100: { start: 20, end: 80 } |
Use playNext/playPrevious without each: true |
Always combine playNext/playPrevious actions with each: true |
Hover/HoverEnd gesture events on window |
Always specify a target element for hover events |
Set flap.type: 'fade' and also animate opacity |
fade type owns opacity — drop opacity from from/to |
TypeScript Types
All types are importable from @motion.page/sdk:
import type {
// Core
AnimationVars,
AnimationConfig,
AnimationEntry,
TargetInput,
ObjectTarget,
AnimationTarget,
EasingFunction,
// Config options
StaggerVars,
RepeatConfig,
SplitType,
PathConfig,
FitConfig,
FlapConfig,
// Trigger configs
HoverConfig,
ClickConfig,
ScrollConfig,
MouseMoveConfig,
MarkerConfig,
PageLoadConfig,
PageExitConfig,
// Gesture
GestureConfig,
GestureEvent,
GestureAction,
GestureInputType,
TimelineAction,
// Cursor
CursorConfig,
CursorStateVars,
CursorSqueezeConfig,
// Context
MotionContext,
} from "@motion.page/sdk";
// Namespace import — all types under Types.*
import { Types } from "@motion.page/sdk";Browser Support
Chrome 90+ · Firefox 90+ · Safari 15+ · Edge 90+
Related: Core Concepts · Getting Started · Sequencing · Stagger · Easing · SPA Integration