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.

What getAnimations() can seeScope decides which layers are included in the result.What getAnimations() can seedocument.getAnimations()Every animation in the documentel.getAnimations({ subtree: true })Element, descendants, pseudo-elementsel.getAnimations()Only effects targeting the element itselfFinished transitionsRemoved from all lists
Scope decides which layers are included in the result.

Step-by-step resolution

Building a page-wide animation inventoryQuery, describe, filter, then act.Building a page-wide animation inventory1Call document.getAnimations() at the moment of interest.A snapshot of live objects2Map each to type, name, target, playState, iterations and progress.Human-readable inventory3Filter infinite or long-running ones for pause controls and audits.The animations that matter for 2.2.24Pause or slow them through the objects themselves.No per-animation CSS needed5Observe new animations by re-querying after interactions.Lists are snapshots, not subscriptions6Assert in tests that reduced motion leaves no long animations running.
Query, describe, filter, then act.

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.

Animations found on a marketing page, by kinddocument.getAnimations() snapshot after load; infinite loops are the ones a pause control must cover.Animations found on a marketing page, by kindFinished CSS animations with fill18 animationsRunning finite entrances6 animationsInfinite CSS loops5 animationsScript animations2 animationsRunning transitions1 animations
document.getAnimations() snapshot after load; infinite loops are the ones a pause control must cover.

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.