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 expose Motion as a global on window:
<script src="https://cloud.motion.page/sdk/latest.js"></script>
<script>
// window.Motion is available globally
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[]): Timeline The 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 playing State — 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 name Callbacks at Positions
Insert callback functions at specific points in the timeline:
tl.call(callback: (...args: unknown[]) => void, params?: unknown[], position?: string | number): this Motion("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): this Cleanup
tl.kill(clearProps?: boolean): void // clearProps=true (default) restores initial CSS
tl.clear(): this // Reset and rebuild without destroying the instance Timeline 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 | Remove [data-scrolltrigger-spacer] and [data-scrolltrigger-markers] DOM nodes |
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?) | Toggle on click |
.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({ 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(); WordPress plugin wraps animation code in Motion.context() automatically. Call MOTIONPAGE_FRONT.reinit() after AJAX content changes.
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