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.

typescript
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

MethodSignatureReturns
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.

typescript
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[].

typescript
Motion.utils.toArray(target, scope?: Element | Document): Element[]
ParameterTypeDescription
targetstring | Element | NodeList | HTMLCollectionWhat to convert
scopeElement | DocumentOptional root for the selector query. Defaults to document
typescript
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:

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

typescript
Motion.utils.clamp(min: number, max: number, value?: number): number | ((value: number) => number)
ParameterTypeDescription
minnumberLower bound
maxnumberUpper bound
valuenumberValue to clamp. Omit to get a curried function
typescript
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:

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

typescript
Motion.utils.random(min: number, max: number, snap?: number): number
ParameterTypeDescription
minnumberLower bound (inclusive)
maxnumberUpper bound (inclusive)
snapnumberOptional snap increment — result rounds to the nearest multiple
typescript
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:

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

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

typescript
Motion.utils.snap(snapTo: number | number[], value?: number): number | ((value: number) => number)
ParameterTypeDescription
snapTonumber | number[]Increment to snap to, or an array of allowed values
valuenumberValue to snap. Omit to get a curried function
typescript
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:

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

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

typescript
Motion.utils.interpolate(start: number, end: number, progress: number): number
ParameterTypeDescription
startnumberValue returned when progress is 0
endnumberValue returned when progress is 1
progressnumberNormalized position — typically 0–1
typescript
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:

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

typescript
Motion.utils.mapRange(
  inMin: number,
  inMax: number,
  outMin: number,
  outMax: number,
  value?: number
): number | ((value: number) => number)
ParameterTypeDescription
inMinnumberInput range lower bound
inMaxnumberInput range upper bound
outMinnumberOutput range lower bound
outMaxnumberOutput range upper bound
valuenumberValue to map. Omit to get a curried function
typescript
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:

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

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

typescript
Motion.utils.normalize(min: number, max: number, value?: number): number | ((value: number) => number)
ParameterTypeDescription
minnumberRange lower bound — maps to 0
maxnumberRange upper bound — maps to 1
valuenumberValue to normalize. Omit to get a curried function
typescript
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:

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

typescript
Motion.utils.wrap(min: number, max: number, value?: number): number | ((value: number) => number)
ParameterTypeDescription
minnumberRange lower bound
maxnumberRange upper bound (exclusive — wraps before reaching it)
valuenumberValue to wrap. Omit to get a curried function
typescript
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:

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

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

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

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

typescript
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

typescript
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