# How to Create Scroll-Triggered Animations with Anime.js: The Complete Guide

> Learn to create scroll-triggered animations using Anime.js. Explore the ScrollObserver API to control animations effortlessly with viewport scroll.

- Repository: [Julian Garnier/anime](https://github.com/juliangarnier/anime)
- Tags: tutorial
- Published: 2026-03-04

---

**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`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/src/events/scroll.js)). The observer calculates progress using:

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
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.

```javascript
// 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`](https://github.com/juliangarnier/anime/blob/main/dist/modules/index.js).
- **Core logic**: Implemented in [`src/events/scroll.js`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/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`](https://github.com/juliangarnier/anime/blob/main/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.