How to Create Scroll-Triggered Animations with Anime.js: The Complete Guide
Anime.js provides a dedicated ScrollObserver API via the onScroll factory that lets you start, control, or synchronize any animation or timeline with the scroll position of the viewport or a specific container.
Scroll-triggered animations are essential for modern interactive web design, and the juliangarnier/anime library offers a lightweight, performant solution through its ScrollObserver architecture. This system decouples scroll handling from the main animation loop, allowing you to trigger, scrub, or link animations directly to scroll progress without manual event listeners.
Understanding the ScrollObserver Architecture
At the heart of Anime.js scroll-triggered animations lies a modular architecture defined primarily in src/events/scroll.js. Understanding these core components is essential for debugging performance issues or extending functionality.
Core Components
-
ScrollObserverclass: Represents a single scroll listener instance. It tracks container scroll position, calculates thresholds, updates progress values, and forwards progress to linked animations. The class handles parsing bounds viaupdateBounds()and manages the scroll synchronization logic inhandleScroll(). -
onScrollfactory: The public entry point exported fromdist/modules/index.js. This factory creates aScrollObserverinstance and registers it in the globalscrollContainersmap, ensuring multiple observers can share the same scroll state efficiently. -
ScrollContainer: An internal helper class that normalizes scroll data (size, offsets, velocity) for each observed container. It runs a lightweight Timer loop usingscrollTicker,dataTimer, andwakeTickerto drive observers without duplicate listeners. -
Timerclass: Located insrc/timer/timer.js, this generic animation engine base class powers both the internal scroll tickers and standard animations.ScrollObserverusesTimerinstances to manage its update cycles independently of the main animation frame. -
scrollContainersmap: A global registry holding aScrollContainerfor every observed element. This design allows multiple animations to respond to the same scroll source without attaching redundant event listeners to the DOM.
How Scroll-Triggered Animations Work
The ScrollObserver system follows a precise execution flow that keeps animations synchronized with scroll position while maintaining 60fps performance.
1. Observer Creation
When you call onScroll({ target, container, ... }), the factory constructs a ScrollObserver and registers the container in scrollContainers. Default thresholds are set to enter: 'end start' and leave: 'start end', meaning the animation triggers when the target's bottom edge reaches the container's top edge.
2. Bounds Parsing
The updateBounds() method converts enter and leave parameters—whether strings like 'top center' or objects like { target: 'top', container: '30%' }—into pixel offsets using parseBoundValue and convertValueUnit helpers.
3. Progress Calculation
On every scroll event, ScrollContainer.handleEvent('scroll') invokes observer.handleScroll() (lines 57‑90 in src/events/scroll.js). The observer calculates progress using:
progress = clamp((scroll - offsetStart) / distance, 0, 1)
4. Animation Synchronization
If the sync option is enabled, the observer seeks the linked animation to duration * progress. The link() method (lines 40‑66) pauses the target animation and establishes this connection, supporting both individual animations and full timelines.
5. Callback Execution
After updating progress, the observer fires contextual callbacks including onEnter, onLeave, onUpdate, and directional variants when thresholds are crossed.
Creating Scroll-Triggered Animations with onScroll
The onScroll factory accepts configuration options including axis for direction control, sync for smooth scrolling behavior, and callback functions for lifecycle events.
Basic Vertical Scroll Trigger
Link a simple animation to trigger when an element enters the viewport. The animation must have autoplay: false to allow the observer to control playback.
import { animate, onScroll } from 'animejs';
// Create observer watching #box
const scrollObs = onScroll({
target: '#box',
onEnter: () => console.log('box entered viewport')
});
// Create animation and link to observer
const boxAnim = animate('#box', {
translateY: 200,
duration: 800,
autoplay: false,
}).link(scrollObs);
When the target's end reaches the container's start, the observer automatically plays the animation.
Horizontal Scroll with Smooth Sync
Use axis: 'x' to monitor horizontal scrolling. The sync parameter accepts a numeric smooth factor between 0 and 1, where lower values create smoother, delayed following behavior.
import { animate, onScroll } from 'animejs';
const horizObs = onScroll({
target: '#track',
axis: 'x',
sync: 0.5, // Smooth factor for half-speed following
onEnter: () => console.log('track entered')
});
animate('#track', {
translateX: 500,
duration: 1500,
autoplay: false
}).link(horizObs);
Syncing Timelines to Scroll
Entire timelines can be scrubbed by scroll progress. Set sync: true for direct 1:1 mapping between scroll position and timeline progress.
import { createTimeline, onScroll } from 'animejs';
const tl = createTimeline({
autoplay: false,
easing: 'easeOutQuad'
})
.add('#elem1', { opacity: [0, 1], duration: 500 })
.add('#elem2', { rotate: 180, duration: 700 }, '-=300');
const scrollTL = onScroll({
target: '#section',
sync: true, // Direct progress mapping
onEnter: () => console.log('section reached')
});
scrollTL.link(tl);
Custom Thresholds and Callbacks
Define precise trigger points using object notation for enter and leave parameters. This example fires when the panel's top aligns with 30% of the container height.
import { animate, onScroll } from 'animejs';
const obs = onScroll({
target: '#panel',
enter: { target: 'top', container: '30%' },
leave: { target: 'bottom', container: '70%' },
onEnter: () => console.log('enter threshold hit'),
onLeave: () => console.log('leave threshold hit'),
onUpdate: (self) => console.log('progress:', self.progress)
});
animate('#panel', { scale: 1.5, autoplay: false }).link(obs);
Cleanup and Memory Management
Remove observers when components unmount to prevent memory leaks. The revert() method removes the observer from the container, stops internal timers, and deletes the container from scrollContainers when no observers remain.
// Remove scroll listener and stop timers
obs.revert();
// Also revert the linked animation to reset styles
boxAnim.revert();
Summary
- Import from:
animejsprovidesonScrollandScrollObserverexported viadist/modules/index.js. - Core logic: Implemented in
src/events/scroll.jswithScrollObserverhandling progress calculations andScrollContainermanaging scroll events. - Linking: Use
.link(scrollObserver)on any animation or timeline withautoplay: falseto grant scroll control. - Sync modes: Pass
sync: truefor direct mapping, or a number (0‑1) for smooth interpolation. - Thresholds: Configure
enterandleavewith string keywords ('top','center','bottom') or percentage objects. - Performance: The
Timer-based architecture insrc/timer/timer.jsensures efficient updates without blocking the main thread. - Cleanup: Always call
revert()on observers to detach listeners and prevent memory leaks.
Frequently Asked Questions
What is the difference between ScrollObserver and onScroll?
ScrollObserver is the class definition that manages individual scroll listeners and progress calculations, while onScroll is a factory function that creates ScrollObserver instances and registers them in the global scrollContainers map. According to the source in src/events/scroll.js, onScroll handles the instantiation boilerplate while ScrollObserver contains the link(), handleScroll(), and revert() methods.
How do I link multiple animations to a single scroll trigger?
Create one ScrollObserver using onScroll(), then call .link(observer) on multiple animation instances. The observer will seek all linked animations to the same progress value calculated from the scroll position. Each linked animation maintains its own duration and easing, but progresses synchronously based on the observer's handleScroll() updates.
Can I use scroll-triggered animations with horizontal scrolling?
Yes. Set the axis option to 'x' when creating the observer. The ScrollObserver checks this axis property in its constructor (lines 30‑38 of src/events/scroll.js) and uses the container's scrollLeft property instead of scrollTop for progress calculations. This works with any scrollable element specified in the container option, defaulting to document.body when omitted.
How do I remove a scroll trigger when the animation completes?
Call observer.revert() to immediately detach the scroll listener and stop the internal Timer instances. If you want to also reset the animation to its initial state, chain animation.revert() afterward. The revert() method in ScrollObserver removes the instance from the parent ScrollContainer and cleans up the scrollContainers map entry when no observers remain for that container.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →