ScreenshotNeo

BlogHow-to

How to Stop Animations from Causing Inconsistent Website Screenshots

Disable animations during Playwright captures, wait for the intended page state, and control other sources of visual drift for more reliable screenshots.

By the ScreenshotNeo team4 October 20267 min read

For Playwright, pass animations: 'disabled' to a screenshot call or visual assertion. This suppresses CSS animations, CSS transitions, and Web Animations during capture. It does not by itself ensure that data, fonts, images, or asynchronous page updates have finished loading, and it cannot eliminate rendering differences between machines.

await page.screenshot({ path: 'page.png', animations: 'disabled' });
await expect(page).toHaveScreenshot({ animations: 'disabled' });

For visual regression tests, Playwright’s toHaveScreenshot() already disables animations by default and waits until two consecutive screenshots match before comparing with the expectation. If a finite animation is fast-forwarded to its end, that may capture a different state than you intend. Trigger and verify the desired UI state before capturing. Playwright screenshot assertion documentation.

1. Disable animations in a Playwright screenshot

Install Playwright Test in your project if it is not already present:

npm init playwright@latest

For a direct capture with the Playwright library or Playwright Test, set the option on the screenshot operation:

await page.goto('https://example.com');
await page.screenshot({
  path: 'page.png',
  animations: 'disabled',
});

In a Playwright Test visual regression test, use the assertion API:

import { test, expect } from '@playwright/test';

test('homepage matches its visual baseline', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page.getByRole('heading', { name: 'Example Domain' })).toBeVisible();
  await expect(page).toHaveScreenshot('homepage.png');
});

The assertion waits for two consecutive captures to produce the same result, then compares the last capture with the stored expectation. This helps with transient visual changes, but it is not a substitute for checking that the application reached the correct semantic state. You can explicitly pass animations: 'disabled' to make the intended behavior clear in the test.

What “disabled” means

  • Finite animations: Playwright fast-forwards them to completion and fires the transitionend event for CSS transitions. The resulting screenshot may show the final state.
  • Infinite animations: Playwright cancels them for the capture, then resumes them afterward. The capture uses their initial state.
  • After capture: Playwright restores animations it canceled. Avoid relying on capture-time suppression to permanently change application state.

If the screenshot must show an in-progress animation, disabling animations is the wrong capture setting. Instead, drive the animation to a deliberate point or use an application-level test hook that produces a deterministic state, then capture that state.

2. Wait for the intended page state

Animation suppression only addresses motion. A page can still change because an API response arrives, a font loads, an image decodes, or client-side code updates the DOM. Wait for an application-specific signal before capturing.

await page.goto('https://example.com/dashboard');
await page.getByRole('heading', { name: 'Dashboard' }).waitFor();
await page.getByTestId('report-status').getByText('Ready').waitFor();
await page.screenshot({ path: 'dashboard.png', animations: 'disabled' });

Prefer a meaningful condition such as a visible heading, completed status, or loaded record over an arbitrary sleep. A fixed delay can be too short on a slow run and waste time on a fast one. If the page has a known, finite asynchronous transition, a short delay may be appropriate, but keep the semantic assertion as the main readiness check.

3. Use screenshot styles for known volatile regions

When a timestamp, live counter, rotating promotion, or other changing region is outside the visual contract, hide or normalize just that region for the screenshot. Playwright’s screenshot style option injects CSS while capturing, including into inner frames and shadow DOM.

await page.screenshot({
  path: 'dashboard.png',
  animations: 'disabled',
  style: `
    [data-testid="live-clock"],
    .rotating-promotion {
      visibility: hidden !important;
    }
  `,
});

For a reusable stylesheet, Playwright Test also supports stylePath in screenshot assertion options:

await expect(page).toHaveScreenshot('dashboard.png', {
  animations: 'disabled',
  stylePath: './visual-test.css',
});
[data-testid="live-clock"] {
  visibility: hidden !important;
}

/* Normalize a value when preserving the region's layout is useful. */
[data-testid="last-updated"] {
  color: transparent !important;
}

Keep exclusions narrow. Hiding a whole panel can conceal layout, content, or styling regressions that the test should catch. If an area matters but its value changes, normalize only the unstable value or use an appropriate screenshot mask.

4. Keep the rendering environment consistent

A stable animation state does not guarantee pixel-identical output across different machines. Operating system, browser version, browser settings, hardware, power source, and headless mode can affect rendering. Generate and compare baselines in the same environment, with the same browser build and viewport. Playwright recommends using the environment where the baseline was generated. Playwright visual comparisons.

  • Use a consistent OS image and browser version in CI and baseline generation.
  • Keep viewport size, device scale factor, color scheme, and headless configuration consistent.
  • Use the same fonts and browser settings wherever practical.
  • When a browser upgrade changes rendering, review and update baselines intentionally instead of masking the difference automatically.

5. Reduced motion is a separate accessibility test

prefers-reduced-motion: reduce represents a user’s preference for less motion. It is useful for testing whether a site offers an accessible reduced-motion experience, but it is not the same as Playwright’s capture-time animation suppression. It also does not guarantee that JavaScript-driven or every Web Animations API effect stops.

Authors can provide a reduced-motion treatment such as:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    scroll-behavior: auto !important;
    transition-duration: 0.01ms !important;
  }
}

This is an implementation pattern, not a required W3C recipe. Preserve understandable state changes and test the actual experience. You can emulate the preference in Chrome DevTools’ Rendering panel. Chrome DevTools CSS media feature emulation and W3C WAI technique C39.

6. Puppeteer and other capture tools

Do not copy Playwright’s animations option into another framework without checking that framework’s current API. Puppeteer’s screenshot guide documents page and element screenshots, but the consulted guide does not establish an equivalent animation-disable option. Puppeteer screenshot guide.

For another runner, use its documented screenshot controls or apply a deliberate test stylesheet and wait for an application readiness signal. Verify the behavior against the framework’s version in your project; screenshot option names and semantics are framework-specific.

7. Troubleshooting inconsistent screenshots

Symptom Likely cause Fix
The screenshot captures a completed transition instead of its midpoint. Playwright fast-forwards finite animations when animations are disabled. Set the desired state explicitly or capture without suppression when testing motion itself.
The screenshot changes even with animations disabled. Data, images, fonts, or asynchronous UI updates are still changing. Wait for a semantic readiness condition and isolate only irrelevant volatile values.
Local screenshots pass, but CI reports diffs. OS, browser build, viewport, settings, or headless mode differs from the baseline environment. Run baseline generation and comparisons in a consistent environment.
A screenshot assertion times out. The page never becomes visually stable, or an assertion’s timeout is too short for the page. Check for continuously changing regions, wait for the intended state, and normalize irrelevant volatility narrowly.
Reduced-motion emulation does not stop a JavaScript animation. The preference is a signal to the page; the application may not honor it for that animation. Implement and test reduced-motion behavior in the app, or use the framework’s screenshot-specific control.
Puppeteer rejects an animation option. The option was copied from Playwright and is not established by Puppeteer’s screenshot guide. Check the current Puppeteer API and use supported page styling or application controls.

8. Performance, reliability, and cost

Disabling animations is a capture-time control and does not require changing production CSS. Screenshot assertions may take longer because they capture repeatedly until two consecutive images match. A page with genuinely continuous visual updates may never settle; remove or normalize only irrelevant movement, and ensure the test still checks meaningful behavior.

Consistent browser and OS environments improve repeatability. Screenshot tests still consume browser execution time and CI resources, so keep the suite focused on representative visual contracts and avoid broad masks that make tests pass without checking the interface. No performance benchmark or fixed cost estimate applies universally; the cost depends on the runner and CI environment.

9. Or skip the browser setup

If you need a screenshot without managing a browser locally, ScreenshotNeo provides a website screenshot API and MCP server. Its capture process accepts cookie and consent banners like a visitor, then 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 the response reports the page verdict and billing status in headers.

Make a one-call capture (see the ScreenshotNeo API documentation):

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);

Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots; and 1,000 screenshots a month are free with no card, with paid plans starting at $5 for 3,000. Sign up for ScreenshotNeo’s free plan.

FAQ

Does disabling animations make screenshots identical on every machine?

No. It controls animation behavior during capture. Browser, operating system, settings, and hardware can still affect rendering.

Should I use reduced motion or the screenshot option?

Use reduced-motion emulation to test the experience offered to users who request less motion. Use the screenshot option to stabilize a Playwright capture. A visual test may need both for separate purposes.

Can I test the animation itself?

Yes. Capture a deliberate animation state without disabling it, and control timing or application state so the expected frame is reproducible.