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, andkeepTime()for animations that must maintain their playhead across refreshes. - Leverage media queries by passing a
mediaQueriesobject tocreateScope()and accessing match states viaself.matchesin your constructors. - Clean up properly by calling
revert()when components unmount or scopes are destroyed, and return cleanup functions fromadd()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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →