Utility Functions
Motion.utils helpers — toArray, clamp, random, snap, interpolate, mapRange.
Motion.utils is a collection of math and DOM helpers for computing dynamic animation values at runtime — converting selectors, clamping progress, mapping ranges for parallax, snapping to grid positions, and more.
Accessing Utilities
Access via the static Motion.utils property, or import MotionUtils directly — both are identical.
import { Motion, MotionUtils } from "@motion.page/sdk";
// Via the static property
const cards = Motion.utils.toArray(".card");
// Via named import — equivalent
const clamped = MotionUtils.clamp(0, 100, 150); // 100 Quick Reference
| Method | Signature | Returns |
|---|---|---|
toArray | (target, scope?) | Element[] |
clamp | (min, max, value?) | number or curried fn |
random | (min, max, snap?) | number |
snap | (snapTo, value?) | number or curried fn |
interpolate | (start, end, progress) | number |
mapRange | (inMin, inMax, outMin, outMax, value?) | number or curried fn |
normalize | (min, max, value?) | number or curried fn |
wrap | (min, max, value?) | number or curried fn |
Curried Functions
Several utilities are curried — omit the final value argument and they return a reusable function instead of computing a result immediately. This lets you configure a helper once and apply it many times.
import { Motion } from "@motion.page/sdk";
// Full call — returns a number immediately
Motion.utils.clamp(0, 100, 150); // 100
// Curried call — returns a configured function
const clamp0to100 = Motion.utils.clamp(0, 100);
clamp0to100(150); // 100
clamp0to100(-50); // 0
clamp0to100(42); // 42 clamp, snap, mapRange, normalize, and wrap all support this pattern. toArray, random, and interpolate do not — they always return a value directly.
toArray
Convert a CSS selector string, NodeList, HTMLCollection, or single Element into a flat Element[].
Motion.utils.toArray(target, scope?: Element | Document): Element[] | Parameter | Type | Description |
|---|---|---|
target | string | Element | NodeList | HTMLCollection | What to convert |
scope | Element | Document | Optional root for the selector query. Defaults to document |
import { Motion } from "@motion.page/sdk";
// From a CSS selector
const cards = Motion.utils.toArray(".card");
// Scoped query — only searches inside .gallery
const container = document.querySelector(".gallery")!;
const items = Motion.utils.toArray(".item", container);
// From a NodeList
const nodes = document.querySelectorAll("section");
const sections = Motion.utils.toArray(nodes); Real-world use — horizontal scroll panel count:
import { Motion } from "@motion.page/sdk";
const panels = Motion.utils.toArray(".panel");
Motion("h-scroll", ".panel", {
to: { x: `-${(panels.length - 1) * 100}%` },
duration: 1,
}).onScroll({
scrub: true,
pin: true,
end: `+=${panels.length * 100}%`,
snap: 1 / (panels.length - 1),
}); clamp
Constrain a number so it never falls below min or rises above max.
Motion.utils.clamp(min: number, max: number, value?: number): number | ((value: number) => number) | Parameter | Type | Description |
|---|---|---|
min | number | Lower bound |
max | number | Upper bound |
value | number | Value to clamp. Omit to get a curried function |
import { Motion } from "@motion.page/sdk";
Motion.utils.clamp(0, 100, 150); // 100
Motion.utils.clamp(0, 100, -20); // 0
Motion.utils.clamp(0, 100, 60); // 60
// Curried — reusable clamp function
const clampProgress = Motion.utils.clamp(0, 1);
clampProgress(1.5); // 1
clampProgress(-0.2); // 0
clampProgress(0.75); // 0.75 Real-world use — guard scroll progress before seeking a timeline:
import { Motion } from "@motion.page/sdk";
const clampProgress = Motion.utils.clamp(0, 1);
Motion("scrub-bar", ".progress-bar", {
to: { width: "100%" },
duration: 1,
}).onPageLoad({ paused: true });
document.addEventListener("scroll", () => {
const raw = window.scrollY / (document.body.scrollHeight - window.innerHeight);
Motion("scrub-bar").progress(clampProgress(raw));
}); random
Generate a random floating-point number between min and max. Pass an optional snap increment to round the result to the nearest multiple.
Motion.utils.random(min: number, max: number, snap?: number): number | Parameter | Type | Description |
|---|---|---|
min | number | Lower bound (inclusive) |
max | number | Upper bound (inclusive) |
snap | number | Optional snap increment — result rounds to the nearest multiple |
import { Motion } from "@motion.page/sdk";
Motion.utils.random(0, 100); // e.g. 63.27
Motion.utils.random(0, 100, 10); // e.g. 60 (snapped to nearest 10)
Motion.utils.random(-50, 50); // e.g. -17.4
Motion.utils.random(0, 1, 0.25); // 0, 0.25, 0.5, 0.75, or 1 Real-world use — organic scattered entrance:
import { Motion } from "@motion.page/sdk";
const { random } = Motion.utils;
Motion("scattered", ".particle", {
from: {
x: random(-200, 200),
y: random(-100, 100),
opacity: 0,
scale: random(0.5, 1.2),
},
duration: random(0.5, 1.0),
stagger: { each: 0.04, from: "random" },
ease: "power2.out",
}).onPageLoad(); Randomised delay per element:
import { Motion } from "@motion.page/sdk";
const { random, toArray } = Motion.utils;
const items = toArray(".grid-item");
// Each item gets a unique random delay at build time
Motion("grid-in", items, {
from: { opacity: 0, y: random(20, 60) },
duration: random(0.4, 0.8),
stagger: { each: 0.05, from: "random" },
ease: "power2.out",
}).onScroll({ scrub: false, toggleActions: "play none none none" }); snap
Snap a number to the nearest multiple of an increment, or to the nearest value within an array.
Motion.utils.snap(snapTo: number | number[], value?: number): number | ((value: number) => number) | Parameter | Type | Description |
|---|---|---|
snapTo | number | number[] | Increment to snap to, or an array of allowed values |
value | number | Value to snap. Omit to get a curried function |
import { Motion } from "@motion.page/sdk";
// Snap to nearest 10
Motion.utils.snap(10, 23); // 20
Motion.utils.snap(10, 25); // 30
Motion.utils.snap(10, 8); // 10
// Snap to the nearest value in an array
Motion.utils.snap([0, 0.33, 0.66, 1], 0.4); // 0.33
Motion.utils.snap([0, 0.33, 0.66, 1], 0.5); // 0.66
// Curried — reusable snap function
const snapToGrid = Motion.utils.snap(50);
snapToGrid(74); // 50
snapToGrid(130); // 150
snapToGrid(275); // 300 Real-world use — carousel index snapping:
import { Motion } from "@motion.page/sdk";
const slides = Motion.utils.toArray(".slide");
const snapToSlide = Motion.utils.snap(1); // snap to nearest whole number
let rawIndex = 0;
document.querySelector(".track")?.addEventListener("pointerup", () => {
const snapped = snapToSlide(rawIndex);
// jump to the nearest whole slide index
}); Real-world use — scroll with discrete snap points:
import { Motion } from "@motion.page/sdk";
const sections = Motion.utils.toArray("section");
const count = sections.length;
// Evenly distributed snap points: [0, 0.25, 0.5, 0.75, 1] for 5 sections
const snapPoints = sections.map((_, i) => i / (count - 1));
Motion("fullpage", ".page-wrapper", {
to: { y: `-${(count - 1) * 100}vh` },
duration: 1,
}).onScroll({
scrub: true,
pin: true,
end: `+=${count * 100}vh`,
snap: snapPoints,
}); interpolate
Linearly interpolate between start and end at a normalized progress value. At progress = 0 the result equals start; at progress = 1 it equals end. Values outside 0–1 extrapolate beyond the endpoints.
Motion.utils.interpolate(start: number, end: number, progress: number): number | Parameter | Type | Description |
|---|---|---|
start | number | Value returned when progress is 0 |
end | number | Value returned when progress is 1 |
progress | number | Normalized position — typically 0–1 |
import { Motion } from "@motion.page/sdk";
Motion.utils.interpolate(0, 100, 0); // 0
Motion.utils.interpolate(0, 100, 0.5); // 50
Motion.utils.interpolate(0, 100, 1); // 100
Motion.utils.interpolate(20, 80, 0.25); // 35
Motion.utils.interpolate(0, 100, 1.5); // 150 (extrapolates) Real-world use — drive a CSS variable from scroll progress:
import { Motion } from "@motion.page/sdk";
const { interpolate, clamp } = Motion.utils;
const clampScroll = clamp(0, 1);
document.addEventListener("scroll", () => {
const progress = clampScroll(window.scrollY / 400);
// Blend from 0px blur to 12px blur as the user scrolls
const blur = interpolate(0, 12, progress);
document.documentElement.style.setProperty("--header-blur", `${blur}px`);
}); mapRange
Map a value from one numeric range to another in a single step. Equivalent to normalizing the input then interpolating to the output, but more concise.
Motion.utils.mapRange(
inMin: number,
inMax: number,
outMin: number,
outMax: number,
value?: number
): number | ((value: number) => number) | Parameter | Type | Description |
|---|---|---|
inMin | number | Input range lower bound |
inMax | number | Input range upper bound |
outMin | number | Output range lower bound |
outMax | number | Output range upper bound |
value | number | Value to map. Omit to get a curried function |
import { Motion } from "@motion.page/sdk";
// Map [0, 500] → [0, 1]
Motion.utils.mapRange(0, 500, 0, 1, 250); // 0.5
// Map [0, 1] → [20, 80]
Motion.utils.mapRange(0, 1, 20, 80, 0.75); // 65
// Inverted range — output decreases as input increases
Motion.utils.mapRange(0, 100, 1, 0, 40); // 0.6
// Curried — scrollY to a parallax offset
const scrollToOffset = Motion.utils.mapRange(0, 600, -60, 60);
scrollToOffset(0); // -60
scrollToOffset(300); // 0
scrollToOffset(600); // 60 Real-world use — parallax layer driven by scroll:
import { Motion } from "@motion.page/sdk";
const mapParallax = Motion.utils.mapRange(0, window.innerHeight, -80, 80);
window.addEventListener("scroll", () => {
Motion.set(".bg-layer", { y: mapParallax(window.scrollY) });
}); Real-world use — 3D card tilt from mouse position:
import { Motion } from "@motion.page/sdk";
const mapX = Motion.utils.mapRange(0, window.innerWidth, -20, 20);
const mapY = Motion.utils.mapRange(0, window.innerHeight, -20, 20);
document.addEventListener("mousemove", (e) => {
Motion.set(".card-3d", {
rotateY: mapX(e.clientX),
rotateX: -mapY(e.clientY),
});
}); normalize
Normalize a value within a [min, max] range to a 0–1 fraction. Equivalent to mapRange(min, max, 0, 1, value) — a dedicated shorthand for the common case.
Motion.utils.normalize(min: number, max: number, value?: number): number | ((value: number) => number) | Parameter | Type | Description |
|---|---|---|
min | number | Range lower bound — maps to 0 |
max | number | Range upper bound — maps to 1 |
value | number | Value to normalize. Omit to get a curried function |
import { Motion } from "@motion.page/sdk";
Motion.utils.normalize(0, 500, 250); // 0.5
Motion.utils.normalize(0, 500, 0); // 0
Motion.utils.normalize(0, 500, 500); // 1
Motion.utils.normalize(100, 200, 150); // 0.5
// Curried — pre-configured for a specific scroll zone
const normSection = Motion.utils.normalize(200, 800);
normSection(200); // 0
normSection(500); // 0.5
normSection(800); // 1 Real-world use — section-local scroll progress:
import { Motion } from "@motion.page/sdk";
const section = document.querySelector<HTMLElement>(".section")!;
const clamp = Motion.utils.clamp(0, 1);
const normalize = Motion.utils.normalize(
section.offsetTop,
section.offsetTop + section.offsetHeight
);
Motion("section-anim", ".section-content", {
from: { opacity: 0, y: 40 },
duration: 1,
}).onPageLoad({ paused: true });
document.addEventListener("scroll", () => {
const progress = clamp(normalize(window.scrollY));
Motion("section-anim").progress(progress);
}); wrap
Wrap a value modularly within a [min, max] range. When the value exceeds max it loops back to min; when it drops below min it loops back from max. Useful for carousel indices, cyclic progress, and infinite loops.
Motion.utils.wrap(min: number, max: number, value?: number): number | ((value: number) => number) | Parameter | Type | Description |
|---|---|---|
min | number | Range lower bound |
max | number | Range upper bound (exclusive — wraps before reaching it) |
value | number | Value to wrap. Omit to get a curried function |
import { Motion } from "@motion.page/sdk";
Motion.utils.wrap(0, 4, 0); // 0
Motion.utils.wrap(0, 4, 3); // 3
Motion.utils.wrap(0, 4, 4); // 0 (wraps back to start)
Motion.utils.wrap(0, 4, 5); // 1
Motion.utils.wrap(0, 4, -1); // 3 (wraps from the other end)
// Curried — loop through 5 slide indices
const wrapSlide = Motion.utils.wrap(0, 5);
wrapSlide(4); // 4
wrapSlide(5); // 0
wrapSlide(6); // 1 Real-world use — infinite carousel next/previous:
import { Motion } from "@motion.page/sdk";
const slides = Motion.utils.toArray(".slide");
const wrapIndex = Motion.utils.wrap(0, slides.length);
let current = 0;
function goTo(index: number) {
current = wrapIndex(index);
Motion("carousel").kill();
Motion("carousel", slides[current], {
from: { x: "100%", opacity: 0 },
duration: 0.5,
ease: "power2.out",
}).play();
}
document.querySelector(".next")?.addEventListener("click", () => goTo(current + 1));
document.querySelector(".prev")?.addEventListener("click", () => goTo(current - 1)); Common Patterns
Parallax Layers with mapRange
Each layer moves at a different speed relative to scroll, creating depth. Use a curried mapRange per layer so the computation is ready before the scroll event fires.
import { Motion } from "@motion.page/sdk";
const mapFar = Motion.utils.mapRange(0, document.body.scrollHeight, -60, 60);
const mapMid = Motion.utils.mapRange(0, document.body.scrollHeight, -30, 30);
const mapNear = Motion.utils.mapRange(0, document.body.scrollHeight, -15, 15);
window.addEventListener("scroll", () => {
const y = window.scrollY;
Motion.set(".layer-far", { y: mapFar(y) });
Motion.set(".layer-mid", { y: mapMid(y) });
Motion.set(".layer-near", { y: mapNear(y) });
}); Fluid Values Clamped to a Viewport Range
Compute a responsive value that scales with viewport width but never goes outside safe bounds.
import { Motion } from "@motion.page/sdk";
const fluidSize = Motion.utils.mapRange(375, 1440, 16, 24);
const clampSize = Motion.utils.clamp(16, 24);
const fontSize = clampSize(fluidSize(window.innerWidth));
document.documentElement.style.setProperty("--base-size", `${fontSize}px`); Clamp + Normalize for Safe Timeline Seeking
Combine normalize and clamp to produce a safe 0–1 progress value from raw scrollY, guarding against overscroll or layout-shift edge cases.
import { Motion } from "@motion.page/sdk";
const el = document.querySelector<HTMLElement>(".pinned-section")!;
const normalize = Motion.utils.normalize(el.offsetTop, el.offsetTop + el.offsetHeight);
const clamp = Motion.utils.clamp(0, 1);
Motion("reveal", ".pinned-content", {
from: { opacity: 0, y: 60 },
duration: 1,
}).onPageLoad({ paused: true });
document.addEventListener("scroll", () => {
Motion("reveal").progress(clamp(normalize(window.scrollY)));
}); Snap Points Built from Element Count
Generate evenly distributed snap values for any number of sections without hardcoding fractions.
import { Motion } from "@motion.page/sdk";
const sections = Motion.utils.toArray("section");
const count = sections.length;
// [0, 0.25, 0.5, 0.75, 1] for 5 sections
const snapPoints = sections.map((_, i) =>
Motion.utils.normalize(0, count - 1, i)
);
Motion("fullpage", ".page-wrapper", {
to: { y: `-${(count - 1) * 100}vh` },
duration: 1,
}).onScroll({
scrub: true,
pin: true,
end: `+=${count * 100}vh`,
snap: snapPoints,
}); API Reference
import { Motion, MotionUtils } from "@motion.page/sdk";
// Motion.utils and MotionUtils are identical
Motion.utils.toArray(target, scope?) // Element[]
Motion.utils.clamp(min, max, value?) // number | fn
Motion.utils.random(min, max, snap?) // number
Motion.utils.snap(snapTo, value?) // number | fn
Motion.utils.interpolate(start, end, progress) // number
Motion.utils.mapRange(inMin, inMax, outMin, outMax, value?) // number | fn
Motion.utils.normalize(min, max, value?) // number | fn
Motion.utils.wrap(min, max, value?) // number | fn Functions marked number | fn are curried — omit the final argument to receive a pre-configured function rather than an immediate result.
Related: Core Concepts · Scroll Trigger · Mouse Movement · Timeline Control