How to Use the Anime.js Scope Feature for Scoped Animations

Use createScope() to isolate animations to specific DOM sections, manage responsive defaults with media queries, and automatically clean up with revert().

Anime.js version 3 introduces a powerful Scope API that enables developers to create encapsulated animation contexts within specific DOM boundaries. The scope feature for scoped animations allows you to restrict animation queries to designated root elements, share default parameters across multiple animations, and handle responsive behavior through integrated media query support.

What Are Scoped Animations in Anime.js?

Scoped animations provide DOM-level encapsulation by restricting animation effects to a specific root element and its children. When you create a scope using createScope(), the library temporarily switches the global execution context so that all subsequent animation calls automatically target elements within your specified root.

This architecture prevents animation conflicts between different page sections and enables modular animation design. According to the source code in src/scope/scope.js (lines 107-121), the execute() method handles this context switching by temporarily modifying globals.scope, globals.root, and globals.defaults to the current scope before running your callbacks.

Creating a Scope with createScope()

The createScope() factory function accepts a configuration object and returns a Scope instance. The constructor, implemented in src/scope/scope.js (lines 41-62), supports three primary parameters:

  • root: A CSS selector string, React ref, Angular element reference, or DOM node that defines the scope boundary. Defaults to document.
  • defaults: An object containing default animation parameters (easing, duration, etc.) that merge with global defaults.
  • mediaQueries: An object mapping names to media query strings, enabling responsive scope refreshes.
import { createScope, animate } from 'animejs';

const scope = createScope({
  root: '.animation-container',
  defaults: { ease: 'out(3)', duration: 800 },
  mediaQueries: { isMobile: '(max-width: 600px)' }
});

Registering Animation Constructors

Scope instances provide three distinct methods for registering animation logic, each serving different lifecycle requirements.

Using add() for Responsive Animations

The add() method registers constructors that execute every time the scope refreshes, such as when media query conditions change. According to src/scope/scope.js (lines 158-169), these constructors are stored in this.constructors and re-executed during refresh().

scope.add(self => {
  // Rebuilds automatically when isMobile changes
  const distance = self.matches.isMobile ? 100 : 300;
  animate('.box', { translateX: distance, duration: 1000 });
});

Using addOnce() for Persistent Animations

For animations that should persist across refreshes without reconstruction, use addOnce(). As implemented in src/scope/scope.js (lines 176-194), these callbacks run once and are never reverted when the scope refreshes.

scope.addOnce(() => {
  // Continuous background rotation survives media query changes
  animate('.background', {
    rotate: 360,
    duration: 4000,
    loop: true,
    ease: 'linear'
  });
});

Using keepTime() for Seamless Transitions

The keepTime() method, defined in src/scope/scope.js (lines 198-212), preserves the animation playhead position when scopes refresh. This prevents jarring resets when responsive parameters change.

scope.keepTime(() => {
  return animate('.progress-bar', {
    width: ['0%', '100%'],
    duration: 5000,
    ease: 'linear',
    loop: true
  });
});

Handling Responsive Design with Media Queries

When you pass a mediaQueries object to createScope(), the library creates MediaQueryList objects and attaches change listeners that automatically trigger refresh() when viewport conditions change. Inside your add() callbacks, you receive a self parameter containing a matches object that reflects the current state of your defined media queries.

The handleEvent method in src/scope/scope.js (lines 217-222) catches these change events, while refresh() (lines 124-141) manages the reconstruction cycle by clearing previous revertibles and running all permanent constructors again.

const scope = createScope({
  root: '.hero-section',
  mediaQueries: {
    isTablet: '(max-width: 1024px)',
    isMobile: '(max-width: 640px)'
  }
})
.add(self => {
  // Access responsive states via self.matches
  const scale = self.matches.isMobile ? 0.8 : self.matches.isTablet ? 1.0 : 1.2;
  animate('.hero-title', { scale, duration: 600 });
});

Cleaning Up Scoped Animations

Proper cleanup prevents memory leaks and event listener accumulation. The scope provides two mechanisms for destruction: automatic media query listener removal and manual revert() invocation.

Manual Revert

Calling revert() immediately stops all animations, removes media query listeners, and clears internal collections. According to src/scope/scope.js (lines 250-252), this method iterates through all revertible objects and executes their cleanup routines.

// Cleanup when navigating away or component unmounting
scope.revert();

Return Cleanup Functions

When using add(), return a cleanup function that removes event listeners or DOM modifications. The scope stores these functions and executes them during refresh() before rebuilding animations.

scope.add(() => {
  const element = document.querySelector('.interactive');
  const handler = () => animate(element, { scale: 1.1 });
  element.addEventListener('click', handler);
  
  // Cleanup function for scope management
  return () => element.removeEventListener('click', handler);
});

Complete Working Example

The following example, adapted from the official playground at tests/playground/scope/index.js, demonstrates combining media queries, persistent animations, and time-keeping across responsive breakpoints:

import { createScope, animate, utils } from 'animejs';

const scope = createScope({
  root: '.animation-stage',
  mediaQueries: { isSmall: '(max-width: 800px)' },
  defaults: { ease: 'linear' }
})
.add(self => {
  const squares = utils.$('.square');

  // Persistent animations that survive refreshes
  self.addOnce(() => animate('.square', {
    y: [0, -50, 0, 50, 0],
    loop: true,
    ease: 'inOut(2)',
    duration: 2500
  }));

  // Time-keeping animation for seamless transitions
  self.keepTime(() => animate('.square', {
    rotate: 360,
    duration: 2000,
    loop: true,
    alternate: true
  }));

  // Interactive effects with cleanup
  function handlePointerEnter() {
    animate(this, { scale: 1.5, ease: 'out(3)', duration: 500 });
  }
  function handlePointerLeave() {
    animate(this, { scale: 1, ease: 'out(3)', duration: 500 });
  }

  squares.forEach($sq => {
    $sq.addEventListener('pointerenter', handlePointerEnter);
    $sq.addEventListener('pointerleave', handlePointerLeave);
  });

  return () => {
    squares.forEach($sq => {
      $sq.removeEventListener('pointerenter', handlePointerEnter);
      $sq.removeEventListener('pointerleave', handlePointerLeave);
    });
  };
});

// Manual cleanup trigger
document.body.addEventListener('click', () => scope.revert());

Summary

  • Use createScope() to establish DOM boundaries and shared defaults for your animations, preventing conflicts between page sections.
  • Choose the right constructor method: add() for responsive animations that rebuild on media query changes, addOnce() for persistent loops, and keepTime() for animations that must maintain their playhead across refreshes.
  • Leverage media queries by passing a mediaQueries object to createScope() and accessing match states via self.matches in your constructors.
  • Clean up properly by calling revert() when components unmount or scopes are destroyed, and return cleanup functions from add() to remove event listeners.

Frequently Asked Questions

What is the difference between add() and addOnce() in Anime.js scopes?

The add() method registers constructors that execute every time the scope refreshes, such as when media query conditions change, making it ideal for responsive animations. In contrast, addOnce() registers constructors that run only once and persist across scope refreshes without being torn down, which is perfect for infinite looping background animations that should not reset when the viewport changes.

How do media queries work with Anime.js scoped animations?

When you pass a mediaQueries object to createScope(), the library creates MediaQueryList objects and attaches change listeners that automatically trigger refresh() when viewport conditions change. Inside your add() callbacks, you receive a self parameter containing a matches object that reflects the current state of your defined media queries, allowing you to adjust animation parameters dynamically without manual event listeners.

Does using keepTime() affect animation performance?

Using keepTime() has minimal performance impact because it simply preserves the internal playhead time of the returned Tickable animation when the scope refreshes, rather than destroying and recreating the animation instance. This method stores the Tickable in this.constructorsOnce and ensures continuity across media query changes, which is particularly useful for progress indicators or ambient animations where resetting the animation state would create a jarring user experience.

When should I manually call revert() on a scope?

You should manually call revert() when your component unmounts, the user navigates away from the page section, or you need to immediately stop all animations and remove event listeners within that scope. The revert() method iterates through all revertible objects, stops animations, removes media query listeners, and executes any cleanup functions returned by your constructors, preventing memory leaks in single-page applications or dynamic web components.

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 →