Documentation menu
Guides
Scroll
scroll ties an animation, or any callback, to how far a page or element has scrolled. Progress runs from 0 to 1 across the whole scroll, or across the stretch where one element passes through the viewport.
Animate with the scroll
Pass the controls of an animation to scroll. The animation stops playing on its own clock and its time follows the scroll: the top of the page is the start, the bottom is the end. Use ease: "linear" so the motion keeps pace with the scrollbar.
import { animate, scroll } from "@animaly/dom";
scroll(animate(".progress", { scaleX: [0, 1] }, { ease: "linear" }));Every value of the call follows the same progress. One scroll event updates all of them in a single pass over the engine's columns, however many elements the animation moves.
import { animate, scroll } from "@animaly/dom";
scroll(
animate(".progress", {
scaleX: [0, 1],
backgroundColor: ["#7c3aed", "#db2777"],
}, { ease: "linear" }),
);import { animate, scroll } from "@animaly/dom";
scroll(
animate(".progress", { opacity: [0, 1, 1, 0] }, {
times: [0, 0.1, 0.9, 1],
ease: "linear",
}),
);Stagger
A staggered animation spreads out along the scroll. Each element's time is (duration + delay) × progress − delay, as in motion, so later elements start further down the page and every element reaches its end at progress 1. With a duration of 1 and stagger(0.5) over three elements, halfway down the page they stand at 50%, 25% and 0% of their motion.
import { animate, scroll, stagger } from "@animaly/dom";
scroll(
animate(".row i", { y: [0, -40] }, {
duration: 1,
ease: "linear",
delay: stagger(0.2),
}),
);A callback instead of an animation
Pass a function to receive the progress and the full scroll info on every frame the position changes.
import { scroll } from "@animaly/dom";
const bar = document.querySelector<HTMLElement>(".progress")!;
scroll((progress) => {
bar.style.transform = `scaleX(${progress})`;
});import { motionValue, scroll } from "@animaly/dom";
const progress = motionValue(0);
const bar = document.querySelector<HTMLElement>(".progress")!;
progress.on("change", (latest) => {
bar.style.transform = `scaleX(${latest})`;
});
scroll((latest) => progress.set(latest));import { animate, motionValue, scroll } from "@animaly/dom";
const smoothed = motionValue(0);
const bar = document.querySelector<HTMLElement>(".progress")!;
smoothed.on("change", (latest) => {
bar.style.transform = `scaleX(${latest})`;
});
scroll((progress) => {
animate(smoothed, progress, { type: "spring", stiffness: 200, damping: 30 });
});Stopping
scroll returns a function that removes the listeners. For an animation it also releases the animation from the scroll.
import { animate, scroll } from "@animaly/dom";
const stop = scroll(animate(".progress", { scaleX: [0, 1] }, { ease: "linear" }));
stop();import { animate, scroll } from "@animaly/dom";
import { useEffect, useRef } from "react";
export function ReadingProgress() {
const bar = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!bar.current) return;
return scroll(animate(bar.current, { scaleX: [0, 1] }, { ease: "linear" }));
}, []);
return <div ref={bar} className="progress" />;
}Track an element
With a target, progress measures the target's trip through the container. offset says where that trip starts and ends. Each entry names a point on the target, then a point on the container: "start end" means the target's top meets the viewport's bottom.
import { animate, scroll } from "@animaly/dom";
const card = document.querySelector(".card")!;
scroll(animate(card, { opacity: [0, 1], y: [60, 0] }, { ease: "linear" }), {
target: card,
offset: ["start end", "end end"],
});import { scroll } from "@animaly/dom";
const card = document.querySelector<HTMLElement>(".card")!;
scroll((progress) => {
card.style.setProperty("--seen", String(progress));
}, { target: card, offset: ["start end", "end start"] });Presets
scrollOffsets holds four common trips, the same as motion's. Enter runs from 0 to 1 while the target comes into view and Exit while it leaves.
import { animate, scroll, scrollOffsets } from "@animaly/dom";
const card = document.querySelector(".card")!;
scroll(animate(card, { opacity: [0, 1] }, { ease: "linear" }), {
target: card,
offset: scrollOffsets.Enter,
});import { animate, scroll, scrollOffsets } from "@animaly/dom";
const card = document.querySelector(".card")!;
scroll(animate(card, { scale: [1, 0.8] }, { ease: "linear" }), {
target: card,
offset: scrollOffsets.Exit,
});Any covers every frame in which part of the target is visible, and All every frame in which all of it is. Both list their later edge first, so their progress falls from 1 to 0 as you scroll down. For a 0 to 1 trip across the whole visible stretch, write the offset out.
import { animate, scroll, scrollOffsets } from "@animaly/dom";
const card = document.querySelector(".card")!;
// 0 when the top edge appears, 1 when the bottom edge leaves.
scroll(animate(card, { rotate: [-8, 8] }, { ease: "linear" }), {
target: card,
offset: ["start end", "end start"],
});
// The preset covers the same stretch, from 1 down to 0.
scroll(animate(card, { opacity: [0.4, 1] }, { ease: "linear" }), {
target: card,
offset: scrollOffsets.Any,
});Offset values
A point is start, center, end, a number from 0 to 1, or a length in px, %, vw or vh. A pair of numbers works the same way as a pair of names.
import { animate, scroll } from "@animaly/dom";
const card = document.querySelector(".card")!;
scroll(animate(card, { opacity: [0, 1] }, { ease: "linear" }), {
target: card,
offset: ["start 80%", "start 40vh"],
});
scroll(animate(card, { y: [40, 0] }, { ease: "linear" }), {
target: card,
offset: ["-100px end", "100px end"],
});import { animate, scroll } from "@animaly/dom";
const card = document.querySelector(".card")!;
scroll(animate(card, { scale: [0.9, 1] }, { ease: "linear" }), {
target: card,
offset: [[0, 1], [0.5, 0.5]],
});Many elements
Give each element its own call so each one tracks its own trip through the viewport.
import { animate, scroll, scrollOffsets } from "@animaly/dom";
document.querySelectorAll(".card").forEach((card) => {
scroll(animate(card, { opacity: [0, 1], x: [-40, 0] }, { ease: "linear" }), {
target: card,
offset: scrollOffsets.Enter,
});
});Scrolling elements
container switches from the page to any scrolling element. axis: "x" measures horizontal scrolling.
import { animate, scroll } from "@animaly/dom";
scroll(animate(".list-progress", { scaleX: [0, 1] }, { ease: "linear" }), {
container: document.querySelector(".list")!,
});import { animate, scroll } from "@animaly/dom";
scroll(animate(".gallery-progress", { scaleX: [0, 1] }, { ease: "linear" }), {
container: document.querySelector(".gallery")!,
axis: "x",
});Progress is measured again when the window or the container resizes. If content grows inside a container that keeps its size, pass trackContentSize.
import { animate, scroll } from "@animaly/dom";
scroll(animate(".list-progress", { scaleX: [0, 1] }, { ease: "linear" }), {
container: document.querySelector(".list")!,
trackContentSize: true,
});scrollInfo
scrollInfo reports both axes with their position, length and velocity, once per frame while scrolling. It takes the same options.
import { scrollInfo } from "@animaly/dom";
scrollInfo(({ y }) => {
console.log(y.current, y.scrollLength, y.progress, y.velocity);
});import { scrollInfo } from "@animaly/dom";
scrollInfo(({ x, y }) => {
console.log(x.progress, y.progress);
}, { container: document.querySelector(".gallery")! });import { animate, scrollInfo } from "@animaly/dom";
const cards = document.querySelectorAll(".card");
scrollInfo(({ y }) => {
const skew = Math.max(-10, Math.min(10, y.velocity / 300));
animate(cards, { skewY: skew }, { type: "spring", stiffness: 300, damping: 30 });
});| Option | Type | Default | What it does |
|---|---|---|---|
| current | number | none | Scroll position in pixels. |
| progress | number | none | 0 to 1 between the resolved offsets. |
| scrollLength | number | none | How far the container can scroll, in pixels. |
| velocity | number | none | Pixels per second, 0 when the last scroll event is more than 50 ms old. |
| offset | number[] | none | The resolved offsets in pixels. |
| containerLength | number | none | Visible size of the container. |
| targetLength | number | none | Size of the target, or of the scrollable content without one. |
| targetOffset | number | none | Distance from the container's start to the target. |
Options
| Option | Type | Default | What it does |
|---|---|---|---|
| container | Element | the page | The element that scrolls. |
| axis | "x" | "y" | "y" | Which direction counts as progress. |
| target | Element | none | Measure progress while this element passes through the container instead of across the whole scroll. |
| offset | ScrollOffset | scrollOffsets.All | Where progress starts and ends. Each entry pairs a point on the target with a point on the container. |
| trackContentSize | boolean | false | Check the content size every frame, for content that grows without resizing the container. |
Scroll-linked animations run on the JS path. With createAnimator({ maxAccelerated }), animations that were handed to CSS animations don't follow the scroll; leave maxAccelerated at its default 0 for elements you pass to scroll.