ScreenshotNeo

BlogHow-to

Puppeteer Screenshots with Animations or Transitions Still Running: How to Disable Them

Puppeteer has no documented screenshot option to disable motion. Use reduced-motion emulation, a temporary CSS override, or the Web Animations API.

By the ScreenshotNeo team4 October 20268 min read

Short answer: Puppeteer’s documented screenshot options do not include a built-in animation-disable switch. Prepare the page before calling page.screenshot(): emulate reduced motion if you want to test the page’s response to that preference, inject temporary CSS to suppress CSS animations and transitions for one capture, or use the Web Animations API to control active animation objects. These approaches solve different problems; none guarantees that page JavaScript stops changing the DOM.

Choose the right way to stop motion

Goal Approach Limit
Capture what a reduced-motion visitor should see Emulate prefers-reduced-motion: reduce The site must respond to the preference.
Suppress CSS motion in one screenshot Inject a temporary CSS override before capture Does not freeze JavaScript-driven changes or every rendering effect.
Seek finite active animations to their end Find animations with document.getAnimations() and call finish() Infinite animations and some playback states cannot be finished; the end state may not be the desired frame.

Puppeteer’s documented capture method is Page.screenshot(); do not assume an animations: 'disabled' option from a different browser automation library exists in your installed Puppeteer version. Check the Puppeteer screenshot API for the version your project uses.

Run a complete Puppeteer capture

The following Node.js example launches Chromium, opens a page, injects a motion override, and writes a screenshot. Install Puppeteer in your project first with npm install puppeteer. Save this as capture.cjs and run node capture.cjs https://example.com.

const puppeteer = require('puppeteer');

async function main() {
  const targetUrl = process.argv[2] || 'https://example.com';
  const browser = await puppeteer.launch({ headless: true });

  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });

    const response = await page.goto(targetUrl, {
      waitUntil: 'networkidle2',
      timeout: 30000,
    });

    if (response && !response.ok()) {
      throw new Error(`Page returned HTTP ${response.status()}`);
    }

    await page.addStyleTag({
      content: `
        *, *::before, *::after {
          animation: none !important;
          transition: none !important;
          scroll-behavior: auto !important;
        }
      `,
    });

    await page.screenshot({ path: 'capture.png', fullPage: true });
  } finally {
    await browser.close();
  }
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

This is a one-shot capture override. If you are using an older Puppeteer release without page.addStyleTag() support in the way shown, inject the same style element with page.evaluate(). If the page has a client-side route or hydration step, wait for the relevant content before taking the screenshot; a network-idle event alone does not prove the page is visually settled.

Option 1: emulate reduced motion

Use reduced-motion emulation when you want the page to behave as it would for a visitor whose operating system requests less motion. This changes the media feature exposed to the page. The site must have CSS or JavaScript that responds to it.

await page.emulateMediaFeatures([
  { name: 'prefers-reduced-motion', value: 'reduce' },
]);
await page.screenshot({ path: 'reduced-motion.png' });

For example, a site may define a @media (prefers-reduced-motion: reduce) rule that removes or shortens motion. If it does not, emulation will not stop its animations. The method is documented in Puppeteer’s media feature API; verify availability and behavior against the release installed in your project because the cited documentation is the next version.

Option 2: suppress CSS animations and transitions

For a screenshot-only override, add CSS after navigation and before capture. The !important declarations help override ordinary author styles:

await page.evaluate(() => {
  const style = document.createElement('style');
  style.dataset.screenshotMotionOverride = 'true';
  style.textContent = `
    *, *::before, *::after {
      animation: none !important;
      transition: none !important;
      scroll-behavior: auto !important;
    }
  `;
  document.head.append(style);
});

await page.screenshot({ path: 'still.png' });

The same idea can be applied earlier with page.addStyleTag(), or after a navigation with page.evaluateOnNewDocument() if your setup requires injection before page scripts run. CSS suppression targets CSS animations and transitions. It does not pause timers, stop JavaScript from updating styles or replacing nodes, or guarantee a stable frame for canvas, video, animated images, or other application-controlled rendering.

To remove the override later, remove the tagged style element:

await page.evaluate(() => {
  document.querySelector('[data-screenshot-motion-override]')?.remove();
});

Option 3: control active Web Animations

document.getAnimations() returns animation objects for CSS animations, CSS transitions, and Web Animations. You can inspect them or attempt to finish finite animations before capture:

await page.evaluate(() => {
  for (const animation of document.getAnimations()) {
    try {
      animation.finish();
    } catch {
      // Infinite animations or invalid playback states need a separate policy.
    }
  }
});

await page.screenshot({ path: 'finished-animations.png' });

finish() seeks an animation to its end in its playback direction. It can throw when the end time is infinite or the playback rate is zero. Catching the error prevents one animation from aborting the capture, but it does not decide what to do with that animation. For loops, choose deliberately: cancel the animation, use a CSS override, or set the desired state explicitly. Finishing every animation can also capture an unintended end state, such as a panel that should be shown midway through an entrance.

For a debugging pass, inspect animation details instead of blindly finishing all of them:

const active = await page.evaluate(() =>
  document.getAnimations().map((animation) => ({
    playState: animation.playState,
    currentTime: animation.currentTime,
    playbackRate: animation.playbackRate,
    effect: animation.effect?.getTiming(),
  }))
);
console.log(active);

Wait for the page state you actually need

  1. Choose the motion policy. Use reduced-motion emulation for preference testing, a CSS override for a still capture, or animation controls for a known animation state.
  2. Navigate and wait for content. Use an appropriate navigation condition, then wait for a selector that identifies the content you need. A page can continue making network requests indefinitely, so network idle is not always suitable.
  3. Apply the motion policy. For reduced-motion testing, emulate before or during page load so scripts and styles can observe the setting. For a CSS override, inject it after the page has created its document and before capture.
  4. Wait for application updates. If scripts replace content or update it after navigation, wait for a stable app-specific selector or state. Do not rely on disabling CSS motion to freeze application logic.
  5. Capture and clean up. Call page.screenshot(), then close the page or browser in a finally block so failures do not leave Chromium running.

When navigation is expected to keep connections open, consider a less strict navigation condition and then wait explicitly for the target element:

await page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: 30000 });
await page.waitForSelector('main article', { timeout: 15000 });
// Apply your selected motion policy, then capture.

Options that affect the final screenshot

  • Viewport and scale: Set the viewport before navigation when responsive layout matters. Use a device scale factor appropriate to the output size.
  • Full-page capture: { fullPage: true } captures beyond the viewport, but lazy content may not appear unless the page has loaded it. Scroll or otherwise trigger lazy loading when required, then wait for the content.
  • Output: Set path for a file. Puppeteer also supports screenshot output options such as image type and quality subject to the installed API version; consult the versioned API reference rather than copying options across versions.
  • Timing: A fixed delay can help with a known delayed update, but it is less reliable than waiting for a meaningful selector or application state. A delay cannot guarantee stability if new updates continue.
  • Scope: The CSS override affects the whole document. If only one region should be still, scope your selectors to that region, while remembering that descendant rules and application code can still affect it.

Troubleshooting

Symptom Likely cause Fix
animations: 'disabled' is rejected or has no effect That option is not part of the documented Puppeteer screenshot options. Prepare the page with reduced-motion emulation, CSS injection, or the Web Animations API.
Reduced-motion screenshot still moves The site does not respond to prefers-reduced-motion, or the moving content is controlled elsewhere. Use a CSS override for CSS motion or inspect the page’s animation objects and application updates.
CSS override is present but a component still changes JavaScript continues updating DOM or inline styles; canvas, video, or animated media may also keep rendering. Wait for or set the application state explicitly. Handle non-CSS media separately.
Capture contains a half-transitioned layout The override was injected too late, the page changed after injection, or the visual update had not completed. Inject before capture, wait for the target state, and confirm the relevant element’s computed style or application state.
finish() throws An animation has infinite timing or a playback state that cannot be finished. Catch per animation and decide whether to cancel, override, or explicitly set that state.
Navigation times out on a page that appears loaded The selected network-idle condition may never occur because of long-lived or recurring requests. Use a less strict navigation condition and wait for a specific content selector.
Screenshot is blank or missing lazy content The content has not loaded, the page state was not ready, or lazy loading was not triggered. Wait for the target content and trigger the required scroll or load behavior before capturing.

Performance, reliability, and cost

Motion handling adds page work before the screenshot, but there is no reliable universal timing figure: the target site, browser, network, and chosen wait condition determine how long capture takes. A short fixed delay may appear fast while producing inconsistent output; waiting for a meaningful page state is usually more repeatable. Reuse a browser process for multiple captures when appropriate, while creating an isolated page or context for each job so cookies, state, and injected CSS do not leak between tasks.

For reliable output, make the viewport, target URL, wait condition, and motion policy explicit. Record navigation failures and timeouts separately from screenshot write failures. Ensure browser cleanup runs even after exceptions. Validate the final image when page content is critical; suppressing motion does not prove that the correct state was captured.

Puppeteer itself is a software dependency rather than a per-screenshot service in this workflow. Budget for the compute and operational work of running Chromium, including browser installation, concurrency, retries, and storage. No benchmark or fixed cost applies across environments.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Send one GET request to capture a URL; see the API documentation for the request and available options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
  • Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed. Response headers say the page verdict and whether it was billed.
  • An MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs.
  • 1,000 screenshots a month are free with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.

Sign up for 1,000 free screenshots a month—no card required.

FAQ

Does Puppeteer support animations: 'disabled' in screenshot options?

That option is not shown in the documented Puppeteer screenshot options referenced here. Use page preparation code and verify against the API version installed in your project.

Does reduced-motion emulation disable every animation?

No. It changes the media preference. The page must honor that preference for its motion to change.

Can CSS alone freeze a page?

No. A CSS override suppresses CSS animations and transitions, but scripts can continue changing the page and other media can keep rendering.

Should I finish or cancel a looping animation?

Choose based on the desired visual state. Finishing an infinite animation can fail; canceling or explicitly setting the state may be more appropriate.