ScreenshotNeo

BlogHow-to

Puppeteer screenshot with animations disabled: reduce visual test flakiness

Puppeteer has no screenshot option to disable animations. Inject test-only CSS, emulate reduced motion, and wait for application state to stabilize captures.

By the ScreenshotNeo team4 October 20267 min read

Puppeteer’s page.screenshot() API has no documented animations: 'disabled' option. For CSS animations and transitions, inject a test-only stylesheet with page.addStyleTag() before capture. Emulating prefers-reduced-motion: reduce can also help when the application responds to that preference. Neither method freezes arbitrary JavaScript-driven motion.

Disable CSS animations and transitions before capture

This runnable example navigates to a page, injects CSS that removes animation and transition durations, and saves a screenshot:

const puppeteer = require('puppeteer');

(async () => {
  const browser = await puppeteer.launch({ headless: true });
  try {
    const page = await browser.newPage();
    await page.setViewport({ width: 1440, height: 900 });
    await page.goto('https://example.com', { waitUntil: 'networkidle2' });

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

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

Install Puppeteer in your project with npm install puppeteer, then save the code as a CommonJS file and run it with Node.js. Change the URL and viewport to match the page and test under review. The selector covers the document, pseudo-elements, CSS animations, transitions, and smooth scrolling. The zero durations are a practical CSS mitigation, not a Puppeteer screenshot setting.

When this changes what you see

Setting durations to zero changes how elements reach their visual state. A transition that would normally move an element from one style to another may appear immediately at its destination; animations may jump to a state determined by their timing rules. This is useful when the test needs a stable static rendering, but it can invalidate a test whose purpose is to verify motion or transition behavior. Keep the stylesheet limited to screenshot tests where motion itself is not under test.

The rule does not pause JavaScript that changes styles, draws to a canvas, or advances an animation with timers or requestAnimationFrame. Animation libraries may also maintain their own state. For those cases, make the application enter a known settled state before capturing.

Use reduced-motion emulation when the app supports it

Puppeteer can make the page report that the user prefers reduced motion:

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

await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.screenshot({ path: 'reduced-motion.png' });

Set the emulated preference before navigation when application code checks it during startup. The preference only affects rendering if the site responds to it, for example with a @media (prefers-reduced-motion: reduce) rule. It is not a universal browser-wide animation switch. The W3C describes this media query as a way for users to prevent motion effects; its technique page notes that some users experience distraction or nausea from animated content. See the W3C WAI reduced-motion technique.

You can combine reduced-motion emulation with the injected CSS: use the preference to exercise the application’s intended accessibility behavior, and add the stylesheet when the screenshot test needs CSS motion suppressed regardless of the app’s implementation.

Make page readiness part of the test

Disabling CSS motion does not guarantee that the page has finished loading data or rendering the state you want. Puppeteer’s screenshot guide demonstrates waiting for network idle, but network activity stopping is not proof that all application state has settled. A page may render data after a request completes, poll continuously, or update from a timer.

Wait for a meaningful application condition where possible, then inject the stylesheet and capture:

await page.goto('https://example.com/dashboard', { waitUntil: 'networkidle2' });
await page.waitForSelector('[data-test="dashboard-ready"]');
await page.addStyleTag({
  content: `*, *::before, *::after {
    animation-duration: 0s !important;
    animation-delay: 0s !important;
    transition-duration: 0s !important;
    transition-delay: 0s !important;
    scroll-behavior: auto !important;
  }`,
});
await page.screenshot({ path: 'dashboard.png', fullPage: true });

Replace the selector with a real readiness marker from the app or test fixture. For JavaScript-driven animation, an app-specific test hook can expose a stable state or stop the relevant animation. Puppeteer’s evaluateOnNewDocument() runs after document creation but before page scripts, which can support a narrowly designed test hook. It does not provide a universal mechanism for freezing every JavaScript animation.

Choose the right control for the source of motion

Source of instability Useful control Limit
CSS animation or transition Inject CSS with zero duration and delay before the screenshot. May change the final visual state; does not stop JavaScript updates.
Application supports reduced motion Emulate prefers-reduced-motion: reduce. Has no effect unless the app responds to the preference.
JavaScript, canvas, timers, or animation library Wait for an app-specific settled state or use a test hook. Requires knowledge of the application’s behavior.
Changing page data or timestamps Use deterministic test data or filter the volatile region in the test setup. Animation suppression does not stabilize changing content.

Puppeteer’s documented screenshot settings include options such as full-page capture and clipping, but not an animation-disabling property. Playwright has a separate documented animations: 'disabled' option for screenshot assertions: finite animations are fast-forwarded, while infinite animations are canceled to their initial state for the screenshot and then play afterward. That is Playwright behavior, not Puppeteer syntax. See the Puppeteer screenshot API, Puppeteer addStyleTag API, and Playwright PageAssertions API.

Troubleshoot flaky screenshots

  • The page still moves after the CSS is injected. The motion may be driven by JavaScript, a canvas, a timer, or code that keeps changing styles. Wait for an application-specific settled state or add a test hook for that animation.
  • The injected CSS appears to have no effect. Confirm that addStyleTag() completed before the screenshot and that the motion is CSS-based. Inspect the affected element and pseudo-elements. If app styles override the rule through unusual cascade behavior, scope a test rule to the relevant elements or add the necessary selector specificity.
  • Reduced-motion emulation does nothing. The page may not implement a reduced-motion response, or the preference may have been set after startup logic ran. Emulate the feature before navigation and check whether the application defines a corresponding media query or JavaScript behavior.
  • The screenshot is stable but shows the wrong state. Zero-duration rules can make a transition reach its end state immediately. Wait for the intended state explicitly, or avoid suppressing motion if the transition itself defines what the test must verify.
  • Captures differ even though motion is disabled. Check for changing data, timestamps, random values, late fonts or images, lazy-loaded content, and viewport differences. Animation control only addresses motion; it does not make external inputs deterministic.
  • networkidle2 never arrives. Pages with persistent connections or background requests may not become idle. Use an appropriate navigation condition and wait for a specific element or application readiness signal instead of assuming the network will go quiet.
  • A fixed sleep sometimes works and sometimes fails. A delay does not identify whether the app reached the desired state. Prefer a selector, explicit test signal, or application hook. Use a timeout as a failure bound, not as proof of readiness.

Keep visual checks reliable and affordable

Use the same viewport, device scale, test data, and readiness condition across runs. Disable only the motion that is irrelevant to the screenshot assertion; otherwise the test can hide regressions in the behavior it should cover. Stabilizing the source of change is generally more useful than increasing screenshot retries, because retries can obscure intermittent failures without explaining them.

For CI, close each browser even on failure, as in the finally block above. Keep screenshots and browser logs for failed runs if your test setup supports artifacts; they can show whether the mismatch came from motion, content, or readiness. No official source cited here quantifies a reduction in flakiness, so treat the CSS and readiness controls as implementation techniques to validate against your application.

Or skip the browser setup

If you need a clean capture of a live page without maintaining a Puppeteer browser, ScreenshotNeo is a website screenshot API and MCP server. Its API accepts one GET request with a URL and returns a PNG, JPEG, WebP, or PDF. See the ScreenshotNeo API docs.

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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

Before capture, ScreenshotNeo accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.

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

FAQ

Can I pass animations: 'disabled' to Puppeteer?

That is not a documented Puppeteer page.screenshot() option. Use page CSS or application-specific controls instead.

Does reduced-motion emulation freeze all animations?

No. It reports a media preference. The page must implement behavior for that preference, and JavaScript-driven animation may need separate handling.

Should I always use networkidle2 before a screenshot?

No. It can be a useful navigation condition, but it does not establish that application state has settled. Wait for the condition that makes the page ready for your test.

Can I use this CSS when testing an animation?

Not if the animation’s behavior is what the assertion needs to cover. Suppressing it changes the behavior under test.