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

  • ScrollObserver class: 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 via updateBounds() and manages the scroll synchronization logic in handleScroll().

  • onScroll factory: The public entry point exported from dist/modules/index.js. This factory creates a ScrollObserver instance and registers it in the global scrollContainers map, 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 using scrollTicker, dataTimer, and wakeTicker to drive observers without duplicate listeners.

  • Timer class: Located in src/timer/timer.js, this generic animation engine base class powers both the internal scroll tickers and standard animations. ScrollObserver uses Timer instances to manage its update cycles independently of the main animation frame.

  • scrollContainers map: A global registry holding a ScrollContainer for 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: animejs provides onScroll and ScrollObserver exported via dist/modules/index.js.
  • Core logic: Implemented in src/events/scroll.js with ScrollObserver handling progress calculations and ScrollContainer managing scroll events.
  • Linking: Use .link(scrollObserver) on any animation or timeline with autoplay: false to grant scroll control.
  • Sync modes: Pass sync: true for direct mapping, or a number (0‑1) for smooth interpolation.
  • Thresholds: Configure enter and leave with string keywords ('top', 'center', 'bottom') or percentage objects.
  • Performance: The Timer-based architecture in src/timer/timer.js ensures 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →