Inspecting Running Animations with getAnimations()
Part of Web Animations API Integration in Core CSS Animation Fundamentals.
The problem
A product needs a “pause all animations” control for accessibility. A test needs to wait until a page’s entrance animations are done before taking a screenshot. A performance investigation needs to know how many animations are running on a slow page, and which ones are infinite. A bug report says “something keeps animating in the background” and nobody knows what.
Each of these requires the same capability: a list of the animations that exist right now, with enough information to identify and control them. That list is one method call away.
Root cause analysis: every animation is an object
Every CSS animation, CSS transition and script animation on a page is represented as an Animation object — specifically CSSAnimation, CSSTransition or plain Animation — attached to the document timeline. These objects exist whether or not you created them in script.
element.getAnimations() returns animations whose effect targets that element. With { subtree: true } it also includes descendants and pseudo-elements such as ::before and ::marker, which are otherwise invisible to element queries.
document.getAnimations() returns every relevant animation targeting elements in the document, sorted in composite order.
The objects are live and mutable. Reading playState tells you whether each one is running, paused, finished or idle. Calling pause() or updatePlaybackRate() on a CSSAnimation changes the real animation, and those changes persist until the CSS changes again — at which point the style system can override script-set play state. Transitions that have finished are removed from the list; CSS animations that have finished with a forwards fill remain.
Identity is readable. CSSAnimation.animationName gives the keyframes name, CSSTransition.transitionProperty gives the property, and script animations can carry an id passed to element.animate(). effect.target and effect.pseudoElement locate the element; effect.getComputedTiming() reports duration, iterations, progress and fill.
Step-by-step resolution
Production code pattern
// 1. Inventory: what is animating on this page right now?
function animationInventory(root = document) {
return root.getAnimations().map((a) => {
const t = a.effect?.getComputedTiming();
const target = a.effect?.target;
return {
type: a.constructor.name,
name: a.animationName ?? a.transitionProperty ?? a.id ?? '(unnamed)',
target: target ? `${target.tagName.toLowerCase()}${target.id ? '#' + target.id : ''}${a.effect.pseudoElement ?? ''}` : '(none)',
playState: a.playState,
infinite: t?.iterations === Infinity,
progress: t?.progress == null ? null : Math.round(t.progress * 100) + '%',
};
});
}
console.table(animationInventory());
// 2. A global pause control for autoplaying and infinite motion.
const pauseButton = document.querySelector('#pause-motion');
pauseButton.addEventListener('click', () => {
const pausing = pauseButton.getAttribute('aria-pressed') !== 'true';
pauseButton.setAttribute('aria-pressed', String(pausing));
document.documentElement.toggleAttribute('data-motion-paused', pausing); // CSS stays in charge
for (const a of document.getAnimations()) {
const infinite = a.effect?.getComputedTiming().iterations === Infinity;
if (!infinite) continue;
pausing ? a.pause() : a.play();
}
});
// 3. Wait for entrances before a screenshot, but never on infinite loops.
async function entrancesSettled() {
const finite = document.getAnimations()
.filter((a) => a.effect?.getComputedTiming().iterations !== Infinity);
await Promise.allSettled(finite.map((a) => a.finished));
}
/* CSS mirrors the pause so animations started later are paused too. */
:root[data-motion-paused] *,
:root[data-motion-paused] *::before,
:root[data-motion-paused] *::after {
animation-play-state: paused !important;
}
@media (prefers-reduced-motion: reduce) {
.ambient, .marquee__track { animation: none; }
}
Rendering Impact: main-thread query, no per-frame cost.
getAnimations()may flush pending style changes so the list is current, which can force a style recalculation if called in the middle of DOM writes; call it at the start of a handler or after a frame.
The CSS rule is what makes the pause durable. Script calls to pause() affect the animations that exist at that moment; an animation that starts afterwards — a newly inserted element, a hover — would run unpaused. The attribute-driven animation-play-state covers future CSS animations, and the script loop handles script-created ones.
Verification checklist
Constraints and trade-offs
- The list is a snapshot; animations created afterwards are not included.
- Script changes to CSS animations can be overridden when the relevant styles change.
- Finished animations with fills clutter inventories; filter by
playState. - Cross-origin iframes and closed shadow roots are not visible from the parent document.
- Pausing an animation mid-way leaves elements in intermediate states; for decorative loops that is fine, for entrances prefer
finish().
Using the inventory in automated tests
The same inventory makes motion testable without screenshots. A reduced-motion test can emulate the preference, load a page, wait a moment and assert that document.getAnimations() contains no running animation with infinite iterations and none longer than a short threshold — a regression check that catches a new component shipping without its reduced-motion branch. A performance smoke test can assert that the number of simultaneously running animations after load stays under a budget, so a page that accidentally starts forty staggered entrances fails before it reaches users.
Keep such assertions tolerant of timing: query after entrances have settled, filter out finished animations that merely hold fills, and compare names rather than counts where specific effects matter. The test harness patterns are covered in writing Playwright tests for motion states.
Frequently asked questions
Does getAnimations() include CSS animations and transitions?
Yes. It returns CSSAnimation and CSSTransition objects alongside animations created with element.animate().
How do I find animations on pseudo-elements?
Call getAnimations({ subtree: true }) on the parent element, or use document.getAnimations(), and read effect.pseudoElement.
Can I pause every animation on a page from script?
You can pause the ones that exist at that moment. Pair it with a CSS rule setting animation-play-state: paused under an attribute so animations that start later are paused too.
Why are finished animations still in the list?
CSS animations that fill forwards remain applied and are still returned. Finished transitions are removed.
Related
- Web Animations API Integration — the parent topic
- Debugging the Animation Effect Stack — using the inventory to find conflicts
- Providing Pause Controls for Autoplay Motion — the accessibility requirement behind the pause control