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

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.

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[]
Parameter Type Description
target string | Element | NodeList | HTMLCollection What to convert
scope Element | Document Optional 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)
Parameter Type Description
min number Lower bound
max number Upper bound
value number Value 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
Parameter Type Description
min number Lower bound (inclusive)
max number Upper bound (inclusive)
snap number Optional 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)
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
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
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
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)
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
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)
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
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)
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
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