npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@motion.page/sdk

v1.0.2

Published

High-performance CSS animation SDK with scroll, hover, gesture, and cursor triggers

Readme

@motion.page/sdk

A high-performance animation SDK with a declarative API. Scroll-triggered animations, page transitions, custom cursors, gesture controls, text splitting, and more — zero runtime dependencies.

npm version Bundle Size License


Table of Contents


Installation

npm install @motion.page/sdk
# or
bun add @motion.page/sdk
# or
yarn add @motion.page/sdk
# or
pnpm add @motion.page/sdk

⚠️ Browser-only: This SDK requires a browser environment (document, window). In SSR frameworks (Next.js, Nuxt, Astro), wrap SDK calls in useEffect, onMounted, or client-side scripts.

For direct browser use without a bundler, see Browser Build.


Quick Start

Basic Animation

import { Motion } from '@motion.page/sdk';

// Fade in and slide up
Motion('hero-intro', '#hero', {
  from: { opacity: 0, y: 50 },
  duration: 0.8,
  ease: 'power2.out',
}).play();

Scroll-Triggered Animation

// Scrub animation progress to scroll position
Motion('scroll-reveal', '.card', {
  from: { opacity: 0, y: 40 },
  duration: 0.6,
}).onScroll({ scrub: true, start: 'top 80%', end: 'top 30%' });

Hover Effect

// Play on hover, reverse on leave
Motion('btn-hover', '.btn', {
  to: { scale: 1.05, backgroundColor: '#0099ff' },
  duration: 0.3,
  ease: 'power2.out',
}).onHover({ onLeave: 'reverse' });

Multi-Step Timeline with Stagger

// Sequence multiple targets; each entry has its own target and position
Motion('intro-sequence', [
  {
    target: '.title',
    from: { opacity: 0, y: -30 },
    duration: 0.6,
  },
  {
    target: '.cards',
    from: { opacity: 0, y: 20 },
    duration: 0.5,
    stagger: { each: 0.1, from: 'start' },
    position: '+=0.1',   // starts 0.1s after the previous entry ends
  },
  {
    target: '.cta',
    from: { opacity: 0, scale: 0.9 },
    duration: 0.4,
    position: '<',        // starts at the same time as the previous entry
  },
]).onPageLoad();

Replay an Existing Timeline

// Retrieve a previously created timeline by name and replay it
Motion('hero-intro').restart();

Object Animation

Plain JavaScript objects can be tweened — useful for canvas, audio, WebGL, or any non-DOM state:

// Animate plain JS objects (useful for canvas, audio, WebGL)
const state = { volume: 0, brightness: 100 };
Motion('audio-fade', state, {
  to: { volume: 1, brightness: 50 },
  duration: 2,
  onUpdate: () => {
    audioNode.gain.value = state.volume;
  },
}).play();

AI Agent Support

This package includes built-in support for AI coding assistants.

llms.txt included — An llms.txt file (per the llmstxt.org standard) ships with the package and is automatically discoverable by AI assistants when @motion.page/sdk is installed in node_modules. It provides a structured index of the entire SDK API so agents can answer questions and generate correct code without hallucinating APIs.

Full skill plugin — For comprehensive AI-assisted development — complete API reference, 50+ examples, and GSAP / Framer Motion migration guides — install the official plugin:

# Universal (40+ agents)
npx skills add motion-page/claude-plugin

# Claude Code
/install-plugin npm:@motion.page/claude-plugin

Core Concept

Every animation in the SDK is a named timeline. The name is the first argument to Motion() and acts as a registry key — calling Motion('same-name') always returns the same Timeline instance.

Timelines are built declaratively via config objects; there are no .to() / .from() method calls. Animation state (targets, transforms, styles) is managed internally by the engine.

If Motion('name', target, config) is called when 'name' already has a timeline, the new entries are appended to the existing timeline rather than replacing it. To rebuild from scratch, call .kill() first:

Motion('hero').kill();
Motion('hero', '#hero', { from: { opacity: 0 }, duration: 0.8 }).onPageLoad();

Implicit Values

The SDK automatically resolves a missing from or to by reading the element's current computed CSS at build time (i.e. when Motion() is first called). This means you rarely need to specify both ends of an animation.

Three cases

| Config | SDK behaviour | |--------|--------------| | from only | Reads current CSS as the to target. Animate from custom values into the element's natural state. | | to only | Reads current CSS as the from starting point. Animate from the natural state to custom values. | | Both | Both endpoints are explicit. Only needed when neither endpoint matches the element's natural CSS. |

Common pattern — reveal animations only need from

// ❌ Redundant — opacity:1 and y:0 are the element's natural CSS defaults
Motion('reveal', '.card', {
  from: { opacity: 0, y: 40 },
  to:   { opacity: 1, y: 0 },
  duration: 0.6,
}).onScroll({ scrub: true });

// ✅ Correct — SDK reads opacity:1 and y:0 from computed CSS automatically
Motion('reveal', '.card', {
  from: { opacity: 0, y: 40 },
  duration: 0.6,
}).onScroll({ scrub: true });

When to only is correct

// Animate FROM the element's current state TO a hover state
Motion('btn-hover', '.btn', {
  to: { scale: 1.05, backgroundColor: '#0099ff' },
  duration: 0.3,
}).onHover({ onLeave: 'reverse' });

When you need both

// Neither endpoint is the element's natural state
Motion('parallax', '.layer', {
  from: { x: -20, y: -20 },
  to:   { x: 20, y: 20 },
}).onMouseMove({ type: 'axis' });

// Animating between two non-default positions
Motion('swipe', '.panel', {
  from: { x: -100 },
  to:   { x: 100 },
}).onGesture({ types: ['touch'], events: { Left: 'play', Right: 'reverse' } });

Natural CSS defaults (common values the SDK resolves automatically)

| Property | Natural default | |----------|----------------| | opacity | 1 | | x, y, z | 0 | | scale, scaleX, scaleY | 1 | | rotate, rotateX, rotateY | 0 | | skewX, skewY | 0 |

Note: height: 'auto' is not a natural default for the animation engine — it must be specified explicitly in to when needed (e.g. accordion reveals).

Build-time vs. play-time

The SDK reads computed CSS at build time (when Motion() is called), not at play time. If the element's styles change after the timeline is created, call .kill() and rebuild the timeline.

Edge case — transform cache

Transform properties (x, y, scale, rotate, etc.) are read from the SDK's internal transform cache rather than getComputedStyle. This ensures composited transforms remain consistent across animations. Plain CSS properties (opacity, color, width, etc.) are read directly from getComputedStyle.


API Reference

Motion Function

Three overloads:

// 1. Retrieve an existing named timeline
Motion(name: string): Timeline

// 2. Create a single-animation timeline
Motion(name: string, target: TargetInput, config: AnimationConfig): Timeline

// 3. Create a multi-step timeline from an array of entries
Motion(name: string, animations: AnimationEntry[]): Timeline

TargetInput accepts: CSS selector string, string[], Element, NodeList, Element[], or a plain object / array of plain objects (for object tweening).

Calling Motion() with the same name on an already-existing timeline returns it unchanged (the retrieve overload). To rebuild a timeline, call .kill() on the old one first.


Motion Static Methods

| Method | Signature | Description | |--------|-----------|-------------| | Motion.set | (target: TargetInput, vars: AnimationVars): void | Immediately apply CSS / transform properties with no animation. Goes through the full animation engine pipeline (including color parsing and transform compositing) but completes in zero time, so applied values persist on the DOM. | | Motion.get | (name: string): Timeline \| undefined | Get a timeline by name; returns undefined if none exists. | | Motion.has | (name: string): boolean | Check whether a named timeline is registered. | | Motion.getNames | (): string[] | Return the names of all registered timelines. | | Motion.kill | (name: string): void | Kill a single timeline by name. Restores initial CSS. | | Motion.killAll | (): void | Kill every timeline and animation managed by the engine. | | Motion.reset | (targets: TargetInput): void | Kill animations targeting those elements, revert text splits, clear transform cache and inline animation styles. | | Motion.refreshScrollTriggers | (): void | Recalculate all scroll trigger start/end positions (call after layout changes). | | Motion.cleanup | (): void | Remove ScrollTrigger spacer and marker DOM nodes from the document. |

Examples

// Immediately hide an element before animating it in
Motion.set('#hero', { opacity: 0, y: -20 });

// Check and conditionally replay
if (Motion.has('hero-intro')) {
  Motion('hero-intro').restart();
}

// Tear down everything (e.g., on page navigation)
Motion.killAll();
Motion.cleanup();

// Reset a specific element to its original state
Motion.reset('.animated-card');

Motion.utils

GSAP-compatible utility functions accessible via Motion.utils. These are drop-in replacements for gsap.utils.* helpers.

MotionUtils is also exported directly from the package and can be imported independently:

import { MotionUtils } from '@motion.page/sdk';
// Same object as Motion.utils

| Method | Signature | Description | |--------|-----------|-------------| | toArray | (target, scope?) → Element[] | Convert CSS selector, NodeList, HTMLCollection, or Element to a flat array. Drop-in for gsap.utils.toArray(). | | clamp | (min, max, value?) → number \| fn | Clamp a value between min and max. Curried if value omitted. | | random | (min, max, snap?) → number | Random number between min and max. Optional snap increment. | | snap | (snapTo, value?) → number \| fn | Snap to nearest increment or array value. Curried if value 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 if value omitted. | | normalize | (min, max, value?) → number \| fn | Normalize a value to 0–1 within a range. Curried if value omitted. | | wrap | (min, max, value?) → number \| fn | Wrap a value within a range (modular arithmetic). Curried if value omitted. |

Examples

// Convert selector to array (replaces gsap.utils.toArray)
const sections = Motion.utils.toArray('.section');
console.log(sections.length); // number of matching elements

// Clamp
Motion.utils.clamp(0, 100, 150);     // 100
const clamp01 = Motion.utils.clamp(0, 1);
clamp01(1.5);                         // 1

// Snap to increment
Motion.utils.snap(5, 13);            // 15
Motion.utils.snap([0, 25, 50], 30);  // 25

// Random with snap
Motion.utils.random(0, 100, 10);     // 0, 10, 20, ..., 100

// Map between ranges
Motion.utils.mapRange(0, 100, 0, 1, 50);  // 0.5

// Normalize to 0–1
Motion.utils.normalize(0, 255, 128);  // ~0.502

// Wrap (modular)
Motion.utils.wrap(0, 360, 450);       // 90

// Interpolate (lerp)
Motion.utils.interpolate(0, 100, 0.5); // 50

Migration from GSAP

| GSAP | Motion SDK | |------|------------| | gsap.utils.toArray('.el') | Motion.utils.toArray('.el') | | gsap.utils.clamp(0, 1, v) | Motion.utils.clamp(0, 1, v) | | gsap.utils.random(0, 100) | Motion.utils.random(0, 100) | | gsap.utils.snap(5, v) | Motion.utils.snap(5, v) | | gsap.utils.mapRange(0, 1, 0, 100, v) | Motion.utils.mapRange(0, 1, 0, 100, v) | | gsap.utils.normalize(0, 100, v) | Motion.utils.normalize(0, 100, v) | | gsap.utils.wrap(0, 360, v) | Motion.utils.wrap(0, 360, v) | | gsap.utils.interpolate(0, 100, 0.5) | Motion.utils.interpolate(0, 100, 0.5) |


Timeline

Timeline is the object returned by every Motion() call. All playback and trigger methods return this for chaining.

Playback Control

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 t=0, restore initial CSS, then play forward
tl.seek(position: number): this   // Jump to a time (seconds) without playing

State — Getter / Setter

Calling with no argument reads the value; calling with an argument sets it and returns this.

tl.duration(): number                      // Read total duration in seconds
tl.progress(value?: number): number | this // Get or set normalized progress (0–1)
tl.time(value?: number): number | this     // Get or set current time in seconds
tl.timeScale(value?: number): number | this // Get or set playback speed multiplier
tl.isActive(): boolean                     // Is the timeline currently animating?
tl.getName(): string | undefined           // Registered name, or undefined for anonymous
// Read
const p = tl.progress();   // e.g. 0.5

// Write (chainable)
tl.progress(0.5).play();
tl.timeScale(2).restart();  // play at 2× speed

Inserting Callbacks at a Position

tl.call(
  callback: (...args: unknown[]) => void,
  params?: unknown[],
  position?: string | number,
): this

Position syntax (same rules apply to AnimationEntry.position):

| Value | Meaning | |-------|---------| | 0.5 | 0.5 s from start (absolute) | | "+=0.5" | 0.5 s after the previous entry ends | | "-=0.3" | 0.3 s before the previous entry ends | | "<" | At the same start time as the previous entry | | ">" | Immediately after the previous entry ends | | "<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('demo', '.box', { from: { opacity: 0 }, duration: 1 })
  .call(() => console.log('halfway'), [], 0.5)
  .call(() => console.log('done'), [], '>');

Cleanup

tl.kill(clearProps?: boolean): void
// Destroy the timeline. clearProps=true (default) restores initial CSS on all targets.

tl.clear(): this
// Reset and rebuild the timeline without destroying it.

Triggers

All trigger methods are chainable and attach behaviour to the timeline without requiring you to manage event listeners manually.

Per-Element Triggers (each: true)

When targeting multiple elements, each: true creates independent per-element timeline instances. Without it, all matched elements share one timeline and play/reverse together.

// WITHOUT each — hovering any card plays ALL cards
Motion('card-hover', '.card', { to: { y: -8 }, duration: 0.3 })
  .onHover({ onLeave: 'reverse' });

// WITH each — each card animates independently
Motion('card-hover', '.card', { to: { y: -8 }, duration: 0.3 })
  .onHover({ each: true, onLeave: 'reverse' });

each is supported by .onHover(), .onClick(), .onScroll(), .onMouseMove(), and .onGesture().

.onHover(config?)

Play on mouseenter, react on mouseleave.

interface HoverConfig {
  target?: string | Element;              // Defaults to animation target(s)
  each?: boolean;                         // Apply trigger to each matched element individually
  onLeave?: 'reverse' | 'pause' | 'stop' | 'restart' | 'none';
  leaveDelay?: number;                    // Seconds to wait before triggering onLeave
}
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' });

.onClick(config?)

Toggle animation on click.

interface ClickConfig {
  target?: string | Element;
  each?: boolean;
  secondTarget?: string | Element;        // Alternative click target
  toggle?: 'reverse' | 'restart' | 'play';
  preventDefault?: boolean;
}
Motion('menu-toggle', '#menu', {
  from: { height: 0, opacity: 0 },
  to:   { height: 'auto' },  // height: 'auto' must be explicit
  duration: 0.4,
  ease: 'power2.inOut',
}).onClick({ target: '#menu-btn', toggle: 'reverse' });

.onScroll(config?)

Scrub or snap animation to scroll position.

interface ScrollConfig {
  target?: string | Element;
  start?: string;                         // e.g. 'top 80%'
  end?: string;                           // e.g. 'bottom 20%'
  scrub?: boolean | number;               // true = instant, number = smoothing seconds
  snap?: number | number[] | ((progress: number) => number);  // Snap scroll progress
  markers?: boolean | MarkerConfig;       // Debug markers (pass object for styling)
  scroller?: string | Element;            // Custom scroll container
  pin?: boolean | string;                 // true = pin animation target; string = pin a different element
  pinSpacing?: boolean | 'margin' | 'padding';
  each?: boolean;
  toggleActions?: string;                 // Format: 'onEnter onLeave onEnterBack onLeaveBack'
}

start / end defaults:

  • Without pin: start: 'top bottom', end: 'bottom top'
  • With pin: start: 'top top', end: 'bottom top'

Relative end with +=: When end starts with +=, the value is a distance measured from the start position:

.onScroll({ start: 'top center', end: '+=800' })    // 800px of scroll travel
.onScroll({ start: 'top top', end: '+=100vh' })     // one viewport height of scroll

pin: string pins a different element (e.g. a parent wrapper) while the animated child scrolls:

// Pin the parent section while the child content animates
Motion('content-reveal', '.content', {
  from: { opacity: 0, y: 40 },
  duration: 1,
}).onScroll({ scrub: true, pin: '.section-wrapper', start: 'top top', end: '+=600' });

toggleActions controls what happens at each scroll boundary. Format: "onEnter onLeave onEnterBack onLeaveBack". Default: "play reverse play reverse". Valid actions: play, pause, resume, reverse, restart, reset, complete, none.

// Play once — never reverse (common for reveal animations)
.onScroll({ toggleActions: 'play none none none' });

// Re-animate every time it enters the viewport
.onScroll({ toggleActions: 'restart none none reset' });

markers accepts true for default debug markers, or a MarkerConfig object for custom styling:

interface MarkerConfig {
  startColor?: string;   // Default: 'green'
  endColor?: string;     // Default: 'red'
  fontSize?: string;     // e.g. '12px'
  fontWeight?: string;
  indent?: number;       // Horizontal offset in pixels
}

snap controls how scroll progress snaps to discrete values:

  • Number — fractional increment to snap to. E.g. snap: 0.25 snaps to 0, 0.25, 0.5, 0.75, 1.
  • Array — explicit progress values to snap to. E.g. snap: [0, 0.33, 0.66, 1].
  • Function — custom snap logic. Receives raw progress (0–1), returns snapped value.

Common pattern for horizontal scroll with equal sections:

const sections = Motion.utils.toArray('.section');
Motion('h-scroll', '.panel', {
  to: { x: `-${(sections.length - 1) * 100}%` },
  duration: 1,
}).onScroll({
  scrub: true,
  snap: 1 / (sections.length - 1),
  pin: true,
  start: 'top top',
  end: `+=${sections.length * 100}%`,
});
Motion('parallax', '.hero-bg', {
  to: { y: -100 },
}).onScroll({ scrub: 1, start: 'top top', end: 'bottom top' });

.onMouseMove(config?)

Drive animation progress from mouse position.

interface MouseMoveConfig {
  type?: 'distance' | 'axis';
  target?: string | Element;              // Element whose bounds define the movement area
  each?: boolean;
  smooth?: number;                        // Smoothing factor (higher = slower follow)
  startProgress?: number;                 // Progress value at rest (0–1)
  leaveProgress?: number;                 // Progress to animate to on mouse leave
}

Defaults:

| Option | Default | Notes | |--------|---------|-------| | type | 'distance' | | | startProgress | 0.5 | Progress value when mouse is at rest | | leaveProgress | 0.5 | Progress to animate to on mouse leave | | smooth | 0.1 | 0 = instant tracking, 1 = maximum lag |

Motion('parallax-depth', '.layer', {
  from: { x: -20, y: -20 },
  to:   { x: 20, y: 20 },
  axis: 'x',  // bind to horizontal axis only
}).onMouseMove({ type: 'axis', smooth: 0.1 });

.onPageLoad()

Play the animation automatically when the page finishes loading. If called after DOMContentLoaded has already fired (common in SPAs or scripts placed at the bottom of <body>), the animation plays immediately.

Motion('page-intro', [
  { target: '.logo',  from: { opacity: 0 }, duration: 0.5 },
  { target: '.nav',   from: { y: -20, opacity: 0 }, duration: 0.4 },
]).onPageLoad();

.onPageExit(config?)

Intercept link clicks, play the exit animation, then navigate to the destination URL after the timeline completes. Works on any website with no server-side dependencies.

interface PageExitConfig {
  /** 'all' (default) | 'include' | 'exclude' */
  mode?: 'all' | 'include' | 'exclude';
  /** CSS selectors for links — required when mode is 'include' or 'exclude' */
  selectors?: string;
  /** Href patterns to skip automatically. Note: 'mailto' also skips tel: links. */
  skipHref?: ('anchor' | 'javascript' | 'mailto')[];
}
  • mode: 'all' (default) — intercepts every <a> on the page
  • mode: 'include' — only links matching selectors
  • mode: 'exclude' — all links except those matching selectors
  • Automatically skips target="_blank" links and modifier-key clicks (Cmd/Ctrl/Shift/Alt)
// Fade-out on page exit — all links
Motion('page-exit', 'body', {
  to: { opacity: 0 },
  duration: 0.4,
  ease: 'power2.in',
}).onPageExit();

// Only internal nav links
Motion('page-exit', 'body', {
  to: { opacity: 0 },
  duration: 0.4,
}).onPageExit({
  mode: 'include',
  selectors: 'nav a',
  skipHref: ['anchor', 'mailto'],
});

.onGesture(config)

Respond to pointer, touch, wheel, or scroll gestures with fine-grained event-to-action mapping.

type GestureInputType = 'pointer' | 'touch' | 'wheel' | 'scroll';

type GestureEvent =
  | 'Up' | 'Down' | 'Left' | 'Right'
  | 'UpComplete' | 'DownComplete' | 'LeftComplete' | 'RightComplete'
  | 'Change' | 'ChangeX' | 'ChangeY'
  | 'ToggleX' | 'ToggleY'
  | 'Press' | 'Release' | 'PressInit'
  | 'Drag' | 'DragEnd'
  | 'Stop' | 'Hover' | 'HoverEnd';

type GestureAction =
  | 'play' | 'pause' | 'reverse' | 'restart' | 'toggle' | 'reset' | 'complete' | 'kill'
  | 'playReverse' | 'progressUp' | 'progressDown' | 'playNext' | 'playPrevious';

interface GestureConfig {
  target?: string | Element;
  types: GestureInputType[];
  events: Partial<Record<GestureEvent, GestureAction>>;
  tolerance?: number;
  dragMinimum?: number;
  wheelSpeed?: number;
  scrollSpeed?: number;
  preventDefault?: boolean;
  lockAxis?: boolean;
  each?: boolean;
  stopDelay?: number;
  animationStep?: number | Partial<Record<GestureEvent, number>>;
  smooth?: number;
}

Config defaults:

| Option | Default | Unit | Notes | |--------|---------|------|-------| | tolerance | 1 | px | Min movement before direction events fire | | dragMinimum | 10 | px | Distance before Drag fires | | wheelSpeed | 1 | multiplier | Scales wheel delta | | scrollSpeed | 1 | multiplier | Scales scroll delta | | stopDelay | 150 | ms | Idle time after movement before Stop fires | | smooth | 0 | 0–1 | Smoothness for progressUp/progressDown actions | | animationStep | 0.1 | 0–1 | Progress step per event for progressUp/progressDown |

Event distinctions:

  • Up/Down/Left/Right — fire continuously during movement
  • UpComplete/DownComplete/etc. — fire once on release if that direction was active
  • PressInit — fires immediately on press, before start position is recorded
  • Press — fires after start position is recorded
  • Hover/HoverEnd — require a target element (not the window)
  • playNext/playPrevious actions — only work when each: true is set

animationStep can be a single number or a per-event map:

animationStep: { Up: 0.2, Down: 0.1 }  // different step size per direction
Motion('swipe-gallery', '.gallery', {
  to: { x: -100 },
}).onGesture({
  types: ['pointer', 'touch'],
  events: {
    Left:  'playNext',
    Right: 'playPrevious',
  },
  dragMinimum: 40,
  lockAxis: true,
});

.onCursor(config)

Replace the native cursor with a fully animated custom cursor.

interface CursorConfig {
  type?: 'basic' | 'text' | 'media';
  smooth?: number;
  squeeze?: boolean | { min?: number; max?: number; multiplier?: number };
  hideNative?: boolean;
  default: CursorStateVars;             // Required: default cursor appearance
  hover?: CursorStateVars;              // State when hovering interactive elements
  click?: CursorStateVars;             // State while pressing
  text?: Record<string, string | number>;
  media?: Record<string, string | number>;
}

CursorStateVars is the shape used for default, hover, and click. The targets field controls which elements trigger that state:

interface CursorStateVars {
  targets?: string[];    // CSS selectors that trigger this state (e.g. ['a', 'button', '.btn'])
  duration?: number;     // Transition duration in seconds (default: 0.15)
  ease?: string;         // Easing (default: 'power3.inOut')
  enabled?: boolean;     // Whether state is active (default: true)
  [key: string]: any;    // Any CSS property: width, height, backgroundColor, scale, etc.
}
Motion('custom-cursor', 'body', {
  to: { opacity: 1 },
  duration: 0,
}).onCursor({
  smooth: 0.08,
  hideNative: true,
  default: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#fff' },
  hover: {
    targets: ['a', 'button', '[data-cursor-hover]'],
    width: 40, height: 40,
    backgroundColor: 'transparent',
    borderColor: '#fff',
  },
  click: { scale: 0.8 },
});

type: 'text' — reads text from mp-cursor-text or mp-cursor-tooltip HTML attributes and displays it inside the cursor element. The text config object sets CSS properties on the text node:

Motion('cursor', 'body', { to: { opacity: 1 }, duration: 0 }).onCursor({
  type: 'text',
  hideNative: true,
  default: { width: 12, height: 12, borderRadius: '50%', backgroundColor: '#fff' },
  hover:   { width: 64, height: 64 },
  text: { fontSize: '12px', color: '#000', fontWeight: 'bold' },
});
<a href="/about" mp-cursor-text="About Us">About</a>
<button mp-cursor-tooltip="Click me">Button</button>

type: 'media' — reads an image or video URL from the mp-cursor-media attribute and renders it inside the cursor. Supports http/https and relative URLs. The media config object sets CSS on the media element:

Motion('cursor', 'body', { to: { opacity: 1 }, duration: 0 }).onCursor({
  type: 'media',
  hideNative: true,
  default: { width: 48, height: 48, borderRadius: '50%' },
  hover:   { width: 120, height: 80 },
  media: { borderRadius: '8px', objectFit: 'cover' },
});
<div class="project-card" mp-cursor-media="/images/preview.jpg">…</div>
<div class="video-card" mp-cursor-media="https://cdn.example.com/preview.mp4">…</div>

Lifecycle Callbacks

Attach callbacks via the AnimationConfig object or directly on the Timeline instance. Both approaches are chainable.

Via AnimationConfig

Motion('slide-in', '.card', {
  from: { opacity: 0, x: -40 },
  duration: 0.6,
  onStart:           () => console.log('started'),
  onUpdate:          (progress) => console.log('progress:', progress),
  onComplete:        () => console.log('done'),
  onRepeat:          (count) => console.log('repeat #', count),
  onReverseComplete: () => console.log('reversed'),
  repeat: { times: 2, yoyo: true, delay: 0.5 },
});

Via Timeline Methods

Motion('slide-in', '.card', { from: { opacity: 0 }, duration: 0.6 })
  .onStart(() => console.log('started'))
  .onUpdate((progress, time) => console.log(progress, time))
  .onComplete(() => console.log('done'));

| Callback | Timeline method | AnimationConfig field | Arguments | |----------|-----------------|-----------------------|-----------| | Start | .onStart(cb) | onStart | none | | Update | .onUpdate(cb) | onUpdate | (progress: number, time: number) via method; (progress: number) via config | | Complete | .onComplete(cb) | onComplete | none | | Repeat | — | onRepeat | (repeatCount: number) | | Reverse complete | — | onReverseComplete | none |


AnimationConfig

Full shape of the config object used for both single-animation (Motion(name, target, config)) and each entry in a multi-step timeline (AnimationEntry).

interface AnimationConfig {
  from?:     AnimationVars;          // Initial values (animated FROM these)
  to?:       AnimationVars;          // Target values (animated TO these)
  duration?: number;                 // Seconds (default: engine default)
  delay?:    number;                 // Seconds before animation begins
  ease?:     string;                 // Easing name string, see Easing section
  stagger?:  number | StaggerVars;  // Per-element stagger delay
  repeat?:   number | RepeatConfig; // Repeat count shorthand or full config (see below)
  split?:    SplitType;             // Text splitting for per-char/word/line animation
  mask?:     boolean;               // Wrap split elements in overflow:hidden for reveal effects
  fit?:      FitConfig;             // FLIP-style morph toward another element
  axis?:     'x' | 'y';            // Axis binding for onMouseMove animations

  // Lifecycle
  onStart?:           () => void;
  onUpdate?:          (progress: number) => void;
  onComplete?:        () => void;
  onRepeat?:          (repeatCount: number) => void;
  onReverseComplete?: () => void;
}

AnimationVars

All animatable properties:

// Transforms
x, y, z                                    // number | string (px default)
rotate, rotateX, rotateY, rotateZ          // number | string (deg default)
scale, scaleX, scaleY, scaleZ              // number
skewX, skewY                               // number | string
perspective                                // number | string

// Visual
opacity                                    // number (0–1)
backgroundColor, color                     // string (CSS color)
filter                                     // string (CSS filter)
transformOrigin                            // string

// Layout
width, height                              // number | string
top, left, right, bottom                   // number | string
margin, marginTop, marginRight, marginBottom, marginLeft
padding, paddingTop, paddingRight, paddingBottom, paddingLeft

// Typography
fontSize, lineHeight, letterSpacing        // number | string
borderRadius                               // number | string
zIndex                                     // number
backgroundPosition                         // string

// Border / outline colors
borderColor, borderTopColor, borderRightColor, borderBottomColor, borderLeftColor
outlineColor, textDecorationColor, caretColor

// SVG
fill, stroke                               // string (CSS color)
drawSVG                                    // string | { start?: number; end?: number }

// Motion path
path: {
  target:   string | Element;             // SVG <path> selector, element, or raw path data (starts with M/m)
  align?:   string | Element;             // Align bounding box to this element
  alignAt?: [number, number];             // Origin point [x%, y%], default [50, 50]
  start?:   number;                       // Path start (0–1), default 0
  end?:     number;                       // Path end (0–1), default 1
  rotate?:  boolean;                      // Auto-rotate along tangent
}

// CSS custom properties
'--my-var'                                 // any CSS custom property name (string)

drawSVG

Animate the visible portion of an SVG stroke. The element must have a stroke and a set stroke-dasharray (or the SDK will compute it automatically).

| Format | Meaning | |--------|---------| | "0% 100%" | Full stroke visible | | "0% 0%" | Stroke fully hidden (start position for a draw-in) | | "20% 80%" | Middle portion only | | "50%" | Shorthand for "0% 50%" | | "100px 500px" | Pixel range along the stroke | | { start: 20, end: 80 } | Object form — values are percentages 0–100, not 0–1 |

// Animate stroke from hidden to fully drawn
Motion('draw-path', 'path#line', {
  from: { drawSVG: '0% 0%' },
  to:   { drawSVG: '0% 100%' },
  duration: 1.2,
  ease: 'power2.inOut',
}).onPageLoad();

// Object format — percentages, not 0–1
Motion('draw-partial', '#circle', {
  to: { drawSVG: { start: 20, end: 80 } },
  duration: 0.8,
}).play();

path

path.target accepts a CSS selector, an Element, or raw SVG path data (a string starting with M or m):

// Inline path data — no DOM element required
Motion('fly', '.icon', {
  to: { path: { target: 'M 0 100 C 50 0 150 200 200 100', rotate: true } },
  duration: 2,
}).play();

CSS Custom Properties

CSS custom properties can be animated by passing the property name as a string key:

Motion('theme', ':root', {
  to: { '--primary-hue': 240, '--accent-opacity': 0.8 },
  duration: 0.5,
}).onClick({ target: '#theme-btn' });

StaggerVars

interface StaggerVars {
  each?:   number;                         // Seconds between each element
  amount?: number;                         // Total stagger spread (alternative to each)
  from?:   'start' | 'center' | 'edges' | 'random' | 'end' | number;
  grid?:   'auto' | [number, number];     // 2D grid stagger
  axis?:   'x' | 'y';                    // Grid stagger axis
  ease?:   string;                         // Easing applied to the stagger distribution
}

RepeatConfig

The repeat field accepts either a plain number or a RepeatConfig object:

// Shorthand — number of additional repetitions
repeat: 3    // repeat 3 more times after the first play
repeat: -1   // repeat infinitely

// Full config
interface RepeatConfig {
  times: number;      // Number of additional repetitions (-1 = infinite)
  delay?: number;     // Seconds between repetitions
  yoyo?: boolean;     // Alternate direction each cycle
}

// Examples
repeat: { times: -1, yoyo: true, delay: 0.2 }  // infinite yoyo with pause between cycles
repeat: { times: 2, yoyo: true, delay: 0.5 }   // 2 extra cycles, yoyo, 0.5s pause

SplitType

type SplitType =
  | 'chars'
  | 'words'
  | 'lines'
  | 'chars,words'
  | 'words,lines'
  | 'chars,words,lines';

Text is split into wrapper <span> elements before animating. Motion.reset() reverts the DOM. Inline elements (like <span class="accent">) are preserved during splitting.

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-mask] | overflow wrapper (when mask: true) | — |

Motion('text-reveal', '.headline', {
  from:     { opacity: 0, y: 20 },
  duration: 0.5,
  split:    'chars',
  stagger:  { each: 0.03, from: 'start' },
}).onPageLoad();

mask

When mask: true is used together with split, each split element (char, word, or line) is wrapped in a parent with overflow: hidden. This clips animated content to its natural bounds, creating a 'reveal' effect — for example, animating y: '100%' makes text slide up from behind an invisible edge.

Motion('reveal', 'h1', {
  split: 'lines',
  mask: true,
  from: { y: '100%' },
  stagger: 0.1,
}).onPageLoad();

FitConfig

FLIP-style morph animation. When fit is set, from and to are ignored — the SDK measures both elements' bounding rects at play time and animates the visual delta between them.

interface FitConfig {
  /** CSS selector for the target element to morph toward */
  target: string;
  /** Convert to absolute positioning during animation. Default: false */
  absolute?: boolean;
  /** Include scale changes. Default: true */
  scale?: boolean;
  /** Animate actual width/height instead of scaleX/scaleY. Default: false */
  resize?: boolean;
}
  • scale: true (default) — animates using scaleX/scaleY. Fast, GPU-accelerated, but can distort text, borders, and box-shadows.
  • resize: true — animates actual width/height properties instead. No visual distortion, but triggers layout reflow. Mutually exclusive with scale.
Motion('reorder', '.container', {
  fit: { target: '.item', resize: true },
  duration: 0.5,
  ease: 'power2.inOut',
}).play();

Easing

Easing names are case-insensitive strings. Pass them to AnimationConfig.ease or StaggerVars.ease.

| Family | Variants | |--------|----------| | linear, none | — | | power1 | power1.in · power1.out · power1.inOut | | power2 | power2.in · power2.out · power2.inOut | | power3 | power3.in · power3.out · power3.inOut | | power4 | power4.in · power4.out · power4.inOut | | sine | sine.in · sine.out · sine.inOut | | expo | expo.in · expo.out · expo.inOut | | circ | circ.in · circ.out · circ.inOut | | back | back.in · back.out · back.inOut | | elastic | elastic.in · elastic.out · elastic.inOut | | bounce | bounce.in · bounce.out · bounce.inOut |

Unknown strings fall back to power1.out.

Motion('spring-in', '.box', {
  from: { scale: 0 },
  duration: 0.8,
  ease: 'elastic.out',
}).play();

Types

All types are re-exported from the package entry point.

Most users won't need to import types directly — TypeScript infers everything from the Motion() function signature, so you get full autocomplete and type checking out of the box. These exports are provided for advanced use cases like building wrapper libraries or typing standalone config objects.

import type {
  // Core
  AnimationVars,
  AnimationConfig,
  AnimationEntry,
  TargetInput,
  ObjectTarget,
  AnimationTarget,
  EasingFunction,

  // Animation options
  StaggerVars,
  RepeatConfig,
  SplitType,
  PathConfig,
  FitConfig,

  // Triggers
  HoverConfig,
  ClickConfig,
  ScrollConfig,
  MouseMoveConfig,
  MarkerConfig,
  PageExitConfig,

  // Gesture
  GestureConfig,
  GestureEvent,
  GestureAction,
  GestureInputType,

  // Cursor
  CursorConfig,
  CursorStateVars,
  CursorSqueezeConfig,
} from '@motion.page/sdk';

// Namespace import
import { Types } from '@motion.page/sdk';

Browser Build

An IIFE build script is included but not part of the default build output. To generate a browser bundle:

bun run packages/sdk/scripts/build-iife.ts

Note: This script is available in the source repository only. It is not included in the npm package.

This creates a self-contained script that exposes window.Motion and window.MotionTimeline:

<script src="motion-sdk.browser.js"></script>
<script>
  const { Motion, MotionTimeline } = window;

  Motion('fade', '.hero', {
    from: { opacity: 0, y: 30 },
    duration: 0.8,
  }).onPageLoad();
</script>

Browser Support

Modern evergreen browsers:

| Browser | Minimum | |---------|---------| | Chrome | 90+ | | Firefox | 90+ | | Safari | 15+ | | Edge | 90+ |


License

FSL-1.1-Apache-2.0Functional Source License, Version 1.1, Apache 2.0 Future License

TL;DR: Free for everyone. Use it in your websites, apps, SaaS products, client projects — commercial or not. One restriction: you can't use it to build a competing animation builder tool.

The one exception: You cannot use this SDK to build a product that competes with Motion.page (e.g., a no-code animation builder, visual animation editor, or similar tool). If you're building something like that, contact us for an enterprise license.

After 2 years from each release, the code converts to the Apache 2.0 license with no restrictions at all.

See LICENSE for the full legal text.