Skip to main content
Anime.js provides a powerful scroll observer system for creating scroll-based animations.

Scroll Observer

The onScroll() function creates scroll-triggered animations.

Basic Usage

import { onScroll } from 'animejs';

const observer = onScroll({
  target: '.box',
  onEnter: (scroll) => {
    console.log('Element entered viewport');
  },
  onLeave: (scroll) => {
    console.log('Element left viewport');
  }
});

Linking Animations

Sync animations with scroll progress:
import { animate, onScroll } from 'animejs';

const animation = animate('.box', {
  translateY: 200,
  rotate: 360,
  duration: 1000,
  autoplay: false
});

onScroll({
  target: '.box',
  sync: true
}).link(animation);
When sync is enabled, the animation’s progress is controlled by the scroll position.

Sync Modes

// Play on enter, pause on leave
onScroll({
  target: '.element',
  sync: 'play pause'
}).link(animation);

// Smooth interpolation
onScroll({
  target: '.element',
  sync: true // or a number between 0-1
}).link(animation);

// With easing
onScroll({
  target: '.element',
  sync: 'out(2)'
}).link(animation);
sync
boolean | number | string | EasingFunction
  • true or 1: Linear sync (default smoothing)
  • 0-1: Custom smoothing factor
  • 'linear': No smoothing
  • 'play pause': Control playback
  • Easing function: Apply easing to scroll progress

Scroll Thresholds

Enter & Leave Points

onScroll({
  target: '.box',
  enter: 'top bottom',  // When top of element hits bottom of viewport
  leave: 'bottom top',  // When bottom of element hits top of viewport
  onEnter: (scroll) => console.log('Entered'),
  onLeave: (scroll) => console.log('Left')
});

Threshold Syntax

// Named positions
enter: 'start end'    // target start, viewport end
enter: 'center center' // target center, viewport center
enter: 'end start'    // target end, viewport start

// Percentage values
enter: '20% 80%'      // 20% of target, 80% of viewport

// Pixel offsets
enter: '100px 500px'

// Relative operators
enter: 'start-100px end' // 100px before target start
enter: 'center+50% bottom' // 50% after target center

// Min/max clamping
enter: 'min+100px end' // At least 100px
enter: 'max-100px start' // At most 100px before max

Object Syntax

onScroll({
  target: '.element',
  enter: {
    container: 'bottom',
    target: 'top'
  },
  leave: {
    container: 'top',
    target: 'bottom'
  }
});

Directional Callbacks

onScroll({
  target: '.box',
  onEnterForward: (scroll) => {
    console.log('Scrolling down, entered');
  },
  onEnterBackward: (scroll) => {
    console.log('Scrolling up, entered');
  },
  onLeaveForward: (scroll) => {
    console.log('Scrolling down, left');
  },
  onLeaveBackward: (scroll) => {
    console.log('Scrolling up, left');
  }
});

Scroll Container

Custom Container

onScroll({
  container: '.scroll-container',
  target: '.box',
  onEnter: (scroll) => {
    console.log('Entered in custom container');
  }
});

Window (Default)

// Uses window as container by default
onScroll({
  target: '.box'
});

Scroll Progress

Access scroll information in callbacks:
onScroll({
  target: '.element',
  onUpdate: (scroll) => {
    console.log('Progress:', scroll.progress);     // 0-1
    console.log('Velocity:', scroll.velocity);     // Scroll speed
    console.log('Backward:', scroll.backward);     // Scroll direction
    console.log('Scroll position:', scroll.scroll);
  }
});

Horizontal Scrolling

onScroll({
  target: '.box',
  axis: 'x',
  enter: 'start end',
  leave: 'end start',
  onEnter: (scroll) => {
    console.log('Entered horizontally');
  }
});

Repeat Behavior

// Trigger every time (default)
onScroll({
  target: '.box',
  repeat: true,
  onEnter: () => console.log('Entered')
});

// Trigger only once
onScroll({
  target: '.box',
  repeat: false,
  onEnter: () => console.log('Entered first time only')
});

Dynamic Parameters

Use functions for dynamic values:
onScroll({
  target: '.box',
  enter: (scroll) => {
    return window.innerWidth > 768 ? 'top bottom' : 'center center';
  },
  axis: (scroll) => {
    return window.innerWidth > 768 ? 'y' : 'x';
  }
});

Debug Mode

Visualize scroll trigger points:
onScroll({
  target: '.box',
  debug: true,
  enter: 'top bottom',
  leave: 'bottom top'
});
Debug mode displays colored overlays showing trigger points and scroll progress.

Complete Example

import { animate, timeline, onScroll } from 'animejs';

// Create animation
const tl = timeline({ autoplay: false });

tl.add('.box', {
  translateY: [100, 0],
  opacity: [0, 1],
  duration: 1000
});

tl.add('.box', {
  rotate: 360,
  scale: [1, 1.5, 1],
  duration: 1500
}, '-=500');

// Link to scroll
const observer = onScroll({
  target: '.box',
  container: window,
  axis: 'y',
  enter: 'top bottom',
  leave: 'bottom top',
  sync: 0.1,
  repeat: true,
  onEnter: (scroll) => {
    console.log('Started', scroll.progress);
  },
  onLeave: (scroll) => {
    console.log('Finished');
  },
  onUpdate: (scroll) => {
    // Update based on scroll
    console.log('Progress:', scroll.progress);
  },
  onSyncComplete: (scroll) => {
    console.log('Sync animation complete');
  },
  onResize: (scroll) => {
    console.log('Container resized');
  }
}).link(tl);

Cleanup

const observer = onScroll({ target: '.box' });

// Remove observer
observer.revert();
Always call revert() when removing elements or cleaning up to prevent memory leaks.

Advanced: Bi-Directional Sync

// Four-way control
onScroll({
  target: '.element',
  sync: 'play pause reverse restart',
  // play: scroll down enter
  // pause: scroll down leave  
  // reverse: scroll up enter
  // restart: scroll up leave
}).link(animation);

Performance Tips

Create one observer per section rather than per element:
onScroll({
  target: '.section',
  onEnter: () => {
    // Animate multiple elements
  }
});
Lower sync values (0-0.3) provide smoother interpolation:
onScroll({
  sync: 0.1 // Smooth but responsive
});
If animation should only trigger once:
onScroll({
  repeat: false
});
Remove observers when elements are destroyed:
const observer = onScroll({ target: '.box' });

// Later...
observer.revert();

Browser Support

Scroll observers use ResizeObserver and IntersectionObserver APIs, supported in all modern browsers.

Build docs developers (and LLMs) love