How to Fix Playwright Screenshot Differences Caused by Animations
Make Playwright screenshots repeatable by disabling animations, isolating dynamic regions, and keeping the rendering environment consistent.
For Playwright visual assertions, use await expect(page).toHaveScreenshot({ animations: 'disabled' }). Screenshot assertions already disable animations by default, so the explicit option documents the test’s intent. For direct page.screenshot() and locator screenshots, set animations: 'disabled' yourself: those capture APIs allow animations by default. If differences remain, target genuinely dynamic regions with a stylesheet or mask, then make the baseline and test use the same rendering environment.
1. Disable animations in the screenshot assertion
In a Playwright Test visual regression test, navigate to the state you want to verify and assert the screenshot:
import { expect, test } from '@playwright/test';
test('page visual state is stable', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot({ animations: 'disabled' });
});
The animations option accepts 'disabled' or 'allow'. With disabled animations, finite animations are fast-forwarded to completion and fire transitionend. Infinite animations are canceled to their initial state for the capture, then played over after capture. These behaviors can affect the state visible in the captured image; choose the setting that matches the intended baseline.
toHaveScreenshot() waits until two consecutive page screenshots match before comparing the result with the expected image. That helps with transient settling, but it does not make changing content deterministic. For the assertion API and options, see the Playwright PageAssertions API and the visual comparisons guide.
2. Set a project-wide assertion default
If your visual assertions should consistently disable animations, configure the option once in the Playwright Test configuration:
import { defineConfig } from '@playwright/test';
export default defineConfig({
expect: {
toHaveScreenshot: { animations: 'disabled' },
},
});
You can still pass assertion options at an individual call site when a test needs a different capture behavior. The TestConfig API documents shared screenshot assertion configuration.
3. Disable animations for direct screenshots
A direct screenshot call has a different default: page.screenshot() allows animations unless you opt out. Set the option explicitly:
import { test } from '@playwright/test';
test('save a stable page screenshot', async ({ page }) => {
await page.goto('/');
await page.screenshot({
path: 'page.png',
animations: 'disabled',
});
});
Locator screenshots support the animation option too. Use the locator when the artifact should contain only one element:
import { test, expect } from '@playwright/test';
test('capture a stable card', async ({ page }) => {
await page.goto('/');
const card = page.locator('[data-testid="product-card"]');
await expect(card).toBeVisible();
await card.screenshot({
path: 'product-card.png',
animations: 'disabled',
});
});
See the Playwright Page API for direct page screenshot options.
4. Isolate dynamic regions that still change
Disabling animation does not freeze every source of changing pixels. Clocks, rotating content, live counters, cursor indicators, and server-provided data may still vary. Keep the screenshot representative by stabilizing test data where possible. If a region is intentionally volatile, target only that region with a screenshot stylesheet or locator mask rather than hiding large parts of the page.
Use a screenshot stylesheet
Page assertion options accept stylePath for a stylesheet applied while capturing. For example, create a file named tests/screenshot.css:
/* tests/screenshot.css */
[data-testid="live-clock"],
[data-testid="rotating-promo"] {
visibility: hidden !important;
}
Then pass it to the assertion:
await expect(page).toHaveScreenshot({
animations: 'disabled',
stylePath: 'tests/screenshot.css',
});
Use selectors specific to volatile content so the snapshot still catches meaningful layout and styling changes. Playwright documents that screenshot stylesheets can filter volatile elements and apply through Shadow DOM and inner frames. See the PageAssertions options.
Mask one volatile locator
If only a specific element needs to be covered, pass a locator in the mask option:
await expect(page).toHaveScreenshot({
animations: 'disabled',
mask: [page.getByTestId('live-clock')],
});
A mask is useful when the changing content itself is irrelevant to the comparison. Avoid broad masks: they can conceal unintended visual regressions in the masked area.
5. Keep baseline and test environments aligned
Even with animations disabled, rendering can differ across host operating systems, browser versions, browser settings, hardware, power sources, and headless versus headed mode. Create and compare snapshots in a consistent environment where practical. When moving a baseline between environments, review the resulting differences before accepting new snapshots. Playwright’s visual comparisons guide describes these sources of variation and snapshot updates.
Update a baseline only after confirming the visual change is intended. Playwright supports updating snapshots with --update-snapshots; updating blindly can make an unintended change appear approved.
6. Troubleshoot remaining differences
| Symptom | Likely cause | Fix |
|---|---|---|
| Assertion screenshots vary despite no animation option | The assertion defaults to disabled animations, but dynamic content or environment variation remains. | Check live data and volatile regions; use a focused stylesheet or mask, and align browser and host settings with the baseline. |
page.screenshot() differs between runs |
Direct page screenshots allow animations by default. | Pass animations: 'disabled' explicitly. |
| A transition ends in an unexpected visual state | Disabled mode fast-forwards finite animations to completion. | Decide whether the completed state is the intended snapshot. If the in-progress state is required, use 'allow' and control timing and page state deliberately. |
| An infinite animation looks different after capture | Disabled mode cancels infinite animations to their initial state for the screenshot, then plays them over afterward. | Use a targeted stylesheet to freeze or hide that element, or capture a stable state designed for the test. |
| Only timestamps or live widgets fail | The content changes independently of CSS animation. | Provide deterministic test data, or mask/style only the specific volatile element. |
| Snapshots differ only on another machine or CI worker | Browser version, operating system, settings, hardware, power source, or headless mode can affect rendering. | Use the same rendering environment for baseline creation and comparison where practical. |
| A broad stylesheet or mask makes the test pass but misses regressions | The volatile area covers stable UI that should still be checked. | Narrow selectors and masks to the smallest changing element, and review the full page for intended behavior. |
| A change disappears after snapshot update | The baseline was refreshed without reviewing whether the difference was correct. | Inspect the diff, confirm the product change is intentional, then update the approved baseline. |
7. Performance, reliability, and cost
Disabling animations adds no separate browser setup: it is a screenshot option. Screenshot assertions already wait for two consecutive matching captures, so slow or continually changing pages can take longer to settle or fail to stabilize. A focused mask or stylesheet can make comparisons more reliable when only a small region is dynamic, while stabilizing test data is usually better when the content should be part of the check.
Snapshot comparison has no per-capture service price in the workflow shown here; the practical costs are test runtime, maintaining baselines, and the compute used by your test environment. Keeping the browser version and host environment consistent reduces avoidable visual churn.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a PNG, JPEG, WebP, or PDF. For a page capture, the API can remove cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf.
For this URL, the one-call request is:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
See the ScreenshotNeo API documentation for request options. 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. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for ScreenshotNeo’s free plan.
FAQ
Does toHaveScreenshot() already disable animations?
Yes. It defaults to disabled animations. Setting the option explicitly makes the test’s choice clear.
Does disabling animations make live page data stable?
No. It controls CSS animations and transitions. Stabilize dynamic data or target volatile regions separately.
Should I update snapshots when a comparison fails?
Only after reviewing the image difference and confirming the new appearance is intended.


