ScreenshotNeo

BlogHow-to

How to Fix Website Screenshot Diffs Caused by Animations

Stop flaky visual comparisons by disabling animation during Playwright screenshot assertions, then handle dynamic regions and rendering differences deliberately.

By the ScreenshotNeo team4 October 20266 min read

For Playwright Test screenshot assertions, pass animations: 'disabled' to toHaveScreenshot(). Playwright disables CSS animations, CSS transitions, and Web Animations while capturing. Finite animations are fast-forwarded to completion; infinite animations are canceled to their initial state for the capture and replayed afterward. The assertion also waits until two consecutive screenshots match before comparing with the expected image.

await expect(page).toHaveScreenshot({ animations: 'disabled' });

This fixes animation timing differences in the screenshot assertion. It does not freeze every changing part of a page or make captures identical across different machines. Use screenshot-only styles for known volatile regions, and keep the browser and rendering environment consistent with the baseline.

Use Playwright’s screenshot assertion option

In a Playwright Test test, add the option directly to the screenshot assertion. The following is a complete minimal test you can save as tests/animation.spec.ts:

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

test('page screenshot is stable while animations run', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot({ animations: 'disabled' });
});

Run it with your project’s Playwright Test command, for example:

npx playwright test tests/animation.spec.ts

On the first run, Playwright may create a baseline screenshot. Review and commit that baseline through your normal visual-test workflow. On later runs, the assertion captures the page, waits for consecutive captures to stabilize, and compares the result with the baseline.

The option is part of Playwright Test’s toHaveScreenshot() assertion API. If you are using a different screenshot library or calling a browser screenshot method directly, this assertion option does not apply; use that tool’s own animation controls or control the page state before capturing. See the Playwright PageAssertions API and Visual comparisons guide.

What disabling animations does

Motion type Behavior with animations: 'disabled'
Finite CSS animations and transitions Fast-forwarded to completion before capture.
Infinite CSS animations and Web Animations Canceled to their initial state for capture, then played again afterward.
Other changing content Not necessarily stabilized by this option; use screenshot styles or control the application state.

This behavior is useful when the baseline should represent a stable page rather than a particular animation frame. If the animation itself is what the test is meant to verify, do not suppress it: arrange a deliberate, repeatable state and test that state instead. That is a test-design choice, not a guarantee supplied by the screenshot option.

Choose the right motion control

Disable motion for a static visual baseline

Use animations: 'disabled' when the test compares a stable screenshot and motion is incidental. It acts at screenshot time, so it does not require the application to implement a reduced-motion mode.

Emulate reduced motion to test the user preference

Use browser-context emulation when the test should verify how the application responds to prefers-reduced-motion: reduce. This is distinct from disabling animation for an individual screenshot assertion. Playwright supports both reduce and no-preference:

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

test.use({ reducedMotion: 'reduce' });

test('page honors reduced motion preference', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page).toHaveScreenshot({ animations: 'disabled' });
});

Use the context setting when the behavior under test is the application’s response to a user preference. Keep the screenshot assertion option when the screenshot itself must have animations disabled. You can test the application’s ordinary motion behavior in a separate test with reducedMotion: 'no-preference'. See Playwright’s Page API for the reduced-motion emulation setting.

Neutralize only a volatile region

If a particular widget continues to change, use the screenshot assertion’s stylePath or screenshot style option to hide or alter that region during capture. For example, a stylesheet can hide a rotating promotional panel while leaving the rest of the page visible:

/* tests/visual-overrides.css */
.rotating-promo {
  visibility: hidden !important;
}
await expect(page).toHaveScreenshot({
  animations: 'disabled',
  stylePath: 'tests/visual-overrides.css',
});

Use a selector that targets only the unstable content. Screenshot styles are applied for capture and can pierce Shadow DOM and inner frames. This keeps the normal page experience available to other tests while preventing an irrelevant moving region from making a visual comparison flaky. Check the installed Playwright version’s API documentation for the exact style option supported in your project.

Troubleshooting persistent screenshot diffs

  1. The option has no effect. Confirm the capture uses Playwright Test’s expect(page).toHaveScreenshot(). The option belongs to that assertion API, not every browser screenshot call.
  2. A widget still changes between captures. It may be dynamic content rather than a CSS animation or Web Animation. Hide or neutralize that identified region with screenshot styles, or make the application state deterministic.
  3. A hover style appears intermittently. The pointer may be over an interactive element. Move it away before capturing when hover state should not be part of the baseline:
    await page.mouse.move(0, 0);
    await expect(page).toHaveScreenshot({ animations: 'disabled' });
  4. Diffs remain across CI and a developer machine. Use a consistent rendering environment. Playwright notes that host operating system, version, settings, hardware, power source, headless mode, and other factors can affect browser rendering. Keep the baseline and comparison environment aligned.
  5. The test is meant to cover reduced-motion behavior. Configure reducedMotion: 'reduce' at the browser-context level. Disabling animations in a screenshot assertion does not, by itself, prove that the application honors the user preference.
  6. Increasing the pixel threshold seems to fix it. A threshold changes comparison tolerance; it does not stabilize the rendered state. First address animation timing, dynamic regions, pointer position, and environment consistency. Then choose a comparison tolerance that fits the project.
  7. The animation is the feature under test. Do not disable it for that assertion. Drive the page to a controlled point in the animation and capture intentionally, so the test checks the desired visual state.

Reliability, performance, and maintenance

Playwright’s screenshot assertion already waits for two consecutive screenshots to match before comparing. Disabling animation addresses one source of instability; it does not make network content, timestamps, random data, layout shifts, or external widgets deterministic. Control those inputs separately or apply narrowly scoped screenshot styles.

Keep visual baselines on the same browser, operating system, and headless configuration where practical. Treat baseline updates as reviewed test changes. Avoid hiding broad page regions: a narrowly targeted override preserves more of the page’s regression coverage.

There is no useful universal timing or cost figure for this option. Capture time depends on the page and test environment. The practical cost is the ongoing maintenance of screenshot baselines and overrides; keep overrides small, documented, and tied to a known volatile element.

Or skip the browser setup

If you need a clean capture without wiring up browser automation, ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF. It is useful for page captures, though it does not replace Playwright assertions when you need to compare a screenshot against a test baseline.

For a complete list of request options, 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);

ScreenshotNeo accepts cookie and consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, failed loads, and cache hits are never billed, and response headers identify the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for AI agents. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000.

Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.

FAQ

Does this remove every source of screenshot flakiness?

No. It targets CSS animations, CSS transitions, and Web Animations during the assertion. Dynamic data and environment differences need separate controls.

Will infinite animations resume after the assertion?

Playwright cancels infinite animations for capture and replays them afterward.

Should every visual test disable animation?

Only when the expected image is meant to represent a static state. Keep motion enabled when animation behavior is what the test validates.

Sources