Scroll-based animation observer system for triggering and synchronizing animations based on scroll position with precise viewport control.
Create scroll-based animation observer with automatic viewport detection and animation synchronization.
/**
* Create scroll observer for triggering animations on scroll
* @param parameters - Scroll observer configuration
* @returns ScrollObserver instance
*/
function onScroll(parameters?: ScrollObserverParams): ScrollObserver;Usage Examples:
import { onScroll, animate } from "animejs";
// Trigger animation when element enters viewport
onScroll({
target: ".box",
onEnter: (self) => {
animate(self.target, {
opacity: [0, 1],
translateY: [50, 0],
});
},
});
// Sync animation progress to scroll position
const animation = animate(
".header",
{
opacity: [1, 0],
translateY: [0, -100],
},
{ autoplay: false }
);
onScroll({
target: ".header",
sync: true,
linked: animation,
});Observe scroll position and control animation playback based on viewport visibility.
/**
* ScrollObserver class for scroll-based animation control
*/
class ScrollObserver {
// ===== Properties =====
/** Unique observer identifier */
id: string | number;
/** Scroll container element */
container: ScrollContainer;
/** Target element being observed */
target: HTMLElement;
/** Linked animation or timer */
linked: Tickable | WAAPIAnimation | null;
/** Whether target is currently in viewport */
isInView: boolean;
/** Whether observer has completed */
completed: boolean;
// ===== Getters =====
/**
* Scroll progress through viewport (0-1)
* 0 = element at enter threshold
* 1 = element at leave threshold
*/
progress: number;
/**
* Current scroll position in pixels
*/
scroll: number;
/**
* Scroll velocity in pixels per frame
* Positive = scrolling down/right
* Negative = scrolling up/left
*/
velocity: number;
/**
* Whether scrolling backward (up or left)
*/
backward: boolean;
// ===== Configuration Properties =====
/**
* Synchronize linked animation to scroll progress
* When true, animation progress matches scroll progress
*/
sync: boolean;
/**
* Repeat callbacks when re-entering viewport
* When false, callbacks only fire once
*/
repeat: boolean;
/**
* Observe horizontal scroll instead of vertical
*/
horizontal: boolean;
/**
* Enter threshold - when element enters viewport
* Number: pixels from container edge
* String: percentage "50%", "top", "center", "bottom"
* Array: [element position, container position]
*/
enter: ScrollThresholdParam;
/**
* Leave threshold - when element leaves viewport
* Same format as enter
*/
leave: ScrollThresholdParam;
// ===== Callbacks =====
/**
* Called when target enters viewport
* Fires in both directions unless using directional callbacks
*/
onEnter: Callback<ScrollObserver>;
/**
* Called when target leaves viewport
* Fires in both directions unless using directional callbacks
*/
onLeave: Callback<ScrollObserver>;
/**
* Called when entering viewport while scrolling forward (down/right)
*/
onEnterForward: Callback<ScrollObserver>;
/**
* Called when leaving viewport while scrolling forward
*/
onLeaveForward: Callback<ScrollObserver>;
/**
* Called when entering viewport while scrolling backward (up/left)
*/
onEnterBackward: Callback<ScrollObserver>;
/**
* Called when leaving viewport while scrolling backward
*/
onLeaveBackward: Callback<ScrollObserver>;
/**
* Called on every scroll update while in range
*/
onUpdate: Callback<ScrollObserver>;
/**
* Called when synchronized animation completes
*/
onSyncComplete: Callback<ScrollObserver>;
// ===== Methods =====
/**
* Link animation to scroll progress
* @param animation - Animation or timer to link
* @returns ScrollObserver instance for chaining
*/
link(animation: Tickable | WAAPIAnimation): ScrollObserver;
/**
* Refresh observer bounds and calculations
* Call after layout changes or element moves
* @returns ScrollObserver instance for chaining
*/
refresh(): ScrollObserver;
/**
* Show debug overlay with threshold lines
* @returns ScrollObserver instance for chaining
*/
debug(): ScrollObserver;
/**
* Remove debug overlay
* @returns ScrollObserver instance for chaining
*/
removeDebug(): ScrollObserver;
/**
* Revert observer and cleanup
* Removes all event listeners and linked animations
* @returns ScrollObserver instance for chaining
*/
revert(): ScrollObserver;
}Usage Examples:
import { onScroll, animate } from "animejs";
// Basic enter/leave callbacks
onScroll({
target: ".fade-in",
enter: "50%", // Enter when element is 50% into viewport
onEnter: (self) => {
animate(self.target, {
opacity: [0, 1],
translateY: [100, 0],
});
},
});
// Directional callbacks
onScroll({
target: ".section",
onEnterForward: (self) => {
console.log("Scrolling down into view");
},
onEnterBackward: (self) => {
console.log("Scrolling up into view");
},
onLeaveForward: (self) => {
console.log("Scrolled past going down");
},
onLeaveBackward: (self) => {
console.log("Scrolled past going up");
},
});
// Sync animation to scroll
const anim = animate(
".parallax",
{
translateY: [0, 200],
},
{ autoplay: false }
);
onScroll({
target: ".parallax",
sync: true,
linked: anim,
onUpdate: (self) => {
console.log(`Scroll progress: ${self.progress}`);
},
});Flexible threshold configuration for precise viewport control:
/**
* Scroll threshold parameter formats
*/
type ScrollThresholdParam =
| number // Pixels from container edge: 100
| string // Percentage or keyword: "50%", "top", "center", "bottom"
| [string | number, string | number]; // [element position, container position]Examples:
import { onScroll } from "animejs";
// Pixel offset from top
onScroll({
target: ".box",
enter: 100, // Enter when 100px from top of viewport
leave: -100, // Leave when 100px past top
});
// Percentage of viewport
onScroll({
target: ".box",
enter: "75%", // Enter when 75% down viewport
leave: "25%", // Leave when 25% down viewport
});
// Keywords
onScroll({
target: ".box",
enter: "bottom", // Enter when element bottom touches viewport bottom
leave: "top", // Leave when element top leaves viewport top
});
// Precise alignment: [element position, container position]
onScroll({
target: ".box",
enter: ["top", "bottom"], // Element top enters at container bottom
leave: ["bottom", "top"], // Element bottom leaves at container top
});
onScroll({
target: ".box",
enter: ["center", "center"], // Element center at viewport center
leave: ["center", "top"], // Element center reaches viewport top
});
// Mix formats
onScroll({
target: ".box",
enter: ["top", "80%"], // Element top enters at 80% down viewport
leave: [100, "top"], // 100px of element past viewport top
});Link animation progress to scroll position:
import { onScroll, animate } from "animejs";
// Create animation without autoplay
const animation = animate(
".parallax-bg",
{
translateY: [0, -500],
scale: [1, 1.2],
},
{ autoplay: false }
);
// Sync to scroll
const observer = onScroll({
target: ".parallax-section",
sync: true,
linked: animation,
enter: "top bottom", // Start when section top hits viewport bottom
leave: "bottom top", // End when section bottom hits viewport top
});
// Animation progress now matches scroll progress
// progress: 0 at enter threshold
// progress: 1 at leave thresholdTrack scroll progress and state:
const observer = onScroll({
target: ".section",
onUpdate: (self) => {
console.log(`Progress: ${self.progress}`); // 0-1 through viewport
console.log(`Scroll: ${self.scroll}px`); // Scroll position
console.log(`Velocity: ${self.velocity}`); // Scroll speed
console.log(`Backward: ${self.backward}`); // Scroll direction
console.log(`In view: ${self.isInView}`); // Visibility state
},
});Control whether animations repeat on each entry:
// Fire once, don't repeat
onScroll({
target: ".animate-once",
repeat: false,
onEnter: (self) => {
animate(self.target, { opacity: [0, 1] });
},
});
// Fire every time entering viewport
onScroll({
target: ".animate-always",
repeat: true,
onEnter: (self) => {
animate(self.target, { opacity: [0, 1] });
},
onLeave: (self) => {
animate(self.target, { opacity: [1, 0] });
},
});Observe horizontal scroll position:
onScroll({
target: ".horizontal-section",
container: ".horizontal-scroller", // Custom scroll container
horizontal: true,
enter: "left right", // Enter from right side
leave: "right left", // Leave from left side
onEnter: (self) => {
animate(self.target, {
opacity: [0, 1],
translateX: [100, 0],
});
},
});Observe scroll in specific container:
onScroll({
target: ".item",
container: ".scroll-container", // Custom scroll container
enter: "top bottom",
leave: "bottom top",
onEnter: (self) => {
animate(self.target, { scale: [0.8, 1] });
},
});Link animations to observer dynamically:
const observer = onScroll({
target: ".element",
sync: true,
});
// Create and link animation later
const animation = animate(
".element",
{
rotate: 360,
scale: [1, 1.5],
},
{ autoplay: false }
);
observer.link(animation);Visual debugging of scroll thresholds:
const observer = onScroll({
target: ".section",
enter: "top 80%",
leave: "bottom 20%",
});
// Show debug overlay with threshold lines
observer.debug();
// Remove debug overlay
observer.removeDebug();Update observer after layout changes:
const observer = onScroll({
target: ".dynamic-content",
sync: true,
linked: animation,
});
// After content loads or layout changes
loadContent().then(() => {
observer.refresh(); // Recalculate bounds and positions
});
// Window resize
window.addEventListener("resize", () => {
observer.refresh();
});Global scroll container registry:
/**
* Map of all scroll containers
* Automatically manages scroll event listeners
*/
const scrollContainers: Map<Element | Window, ScrollContainer>;Usage Example:
import { scrollContainers } from "animejs/events";
// Access scroll containers
for (const [element, container] of scrollContainers) {
console.log(element, container);
}const background = animate(
".bg",
{ translateY: [0, -300] },
{ autoplay: false }
);
const foreground = animate(
".fg",
{ translateY: [0, -600] },
{ autoplay: false }
);
onScroll({
target: ".parallax-section",
sync: true,
linked: background,
});
onScroll({
target: ".parallax-section",
sync: true,
linked: foreground,
});const header = document.querySelector(".header");
onScroll({
target: document.body,
enter: 0,
leave: 200,
onUpdate: (self) => {
header.style.opacity = 1 - self.progress;
},
});document.querySelectorAll(".reveal").forEach((el, i) => {
onScroll({
target: el,
enter: "top 80%",
onEnter: () => {
animate(el, {
opacity: [0, 1],
translateY: [50, 0],
delay: i * 100, // Stagger based on index
});
},
});
});import { createTimeline, onScroll } from "animejs";
const tl = createTimeline({ autoplay: false });
tl.add(".box1", { x: 100 })
.add(".box2", { y: 100 })
.add(".box3", { rotate: 360 });
onScroll({
target: ".timeline-section",
sync: true,
linked: tl,
enter: "top bottom",
leave: "bottom top",
});/**
* Scroll observer configuration parameters
*/
interface ScrollObserverParams {
/** Observer identifier */
id?: string | number;
/** Target element to observe (required if not linking existing animation) */
target?: string | HTMLElement;
/** Scroll container (defaults to window) */
container?: string | HTMLElement;
/** Animation to link to scroll progress */
linked?: Tickable | WAAPIAnimation;
/** Synchronize animation to scroll progress */
sync?: boolean;
/** Repeat callbacks on re-entry */
repeat?: boolean;
/** Observe horizontal scroll */
horizontal?: boolean;
/** Enter viewport threshold */
enter?: ScrollThresholdParam;
/** Leave viewport threshold */
leave?: ScrollThresholdParam;
/** Called when entering viewport */
onEnter?: Callback<ScrollObserver>;
/** Called when leaving viewport */
onLeave?: Callback<ScrollObserver>;
/** Called when entering while scrolling forward */
onEnterForward?: Callback<ScrollObserver>;
/** Called when leaving while scrolling forward */
onLeaveForward?: Callback<ScrollObserver>;
/** Called when entering while scrolling backward */
onEnterBackward?: Callback<ScrollObserver>;
/** Called when leaving while scrolling backward */
onLeaveBackward?: Callback<ScrollObserver>;
/** Called on scroll update */
onUpdate?: Callback<ScrollObserver>;
/** Called when synchronized animation completes */
onSyncComplete?: Callback<ScrollObserver>;
}
/**
* Scroll threshold parameter
*/
type ScrollThresholdParam =
| number // Pixels from edge
| string // Percentage or keyword
| [string | number, string | number]; // [element pos, container pos]
/**
* Scroll container wrapper
*/
interface ScrollContainer {
element: Element | Window;
scrollTop: number;
scrollLeft: number;
scrollHeight: number;
scrollWidth: number;
clientHeight: number;
clientWidth: number;
}
/**
* Callback function type
*/
type Callback<T> = (instance: T) => void;