ScreenshotNeo

BlogHow-to

How to Handle Animations in Playwright Screenshots

Disable or allow Playwright animations correctly, stabilize visual tests, and troubleshoot flaky screenshots with runnable examples.

By the ScreenshotNeo team1 October 20266 min read

For a deterministic Playwright screenshot, pass animations: 'disabled'. This stops CSS animations, CSS transitions, and Web Animations. Finite animations are fast-forwarded to completion; infinite animations are canceled at their initial state during capture and replayed afterward. Direct page and locator screenshots otherwise default to 'allow'. Playwright page screenshot documentation and the locator API documentation describe the same option.

Disable animations in a page screenshot

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: 1440, height: 900 }
});

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

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

await browser.close();

The option belongs on the screenshot call. It is not a browser launch flag and it does not permanently change the page’s CSS.

What animations: 'disabled' actually does

  • CSS animations: stopped for the capture.
  • CSS transitions: stopped for the capture.
  • Web Animations: stopped for the capture.
  • Finite animations: fast-forwarded to their end state. This can fire transitionend.
  • Infinite animations: canceled to their initial state while the screenshot is taken, then replayed.

Therefore, “disabled” does not mean “pause every animation at the current frame.” If you need the visual state at the instant of capture, use animations: 'allow' and control timing separately.

Capture the current animated frame

await page.screenshot({
  path: 'current-frame.png',
  animations: 'allow'
});

'allow' is also the default for direct page and locator screenshots. The result can vary between runs because the animation may be at a different frame when Chromium captures it.

Disable animations for one element

const hero = page.locator('[data-testid="hero"]');

await hero.screenshot({
  path: 'hero.png',
  animations: 'disabled'
});

Locator screenshots support the same setting. This is useful when the page can remain live but a specific animated component must be stable.

Use screenshot assertions for visual regression tests

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

test('checkout page is stable', async ({ page }) => {
  await page.goto('https://example.com/checkout');

  await expect(page).toHaveScreenshot('checkout.png', {
    fullPage: true
  });
});

In Playwright Test, toHaveScreenshot() disables animations by default and waits until two consecutive screenshots match before comparing them. Locator assertions such as await expect(page.locator('.cart')).toHaveScreenshot() behave the same way. See the official page assertions and locator assertions references.

Override the assertion behavior

await expect(page).toHaveScreenshot('live-frame.png', {
  animations: 'allow'
});

Use this only when motion is part of what you intend to verify. For ordinary regression snapshots, leave the assertion default disabled.

Complete stabilization example

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({
  viewport: { width: 1280, height: 800 },
  deviceScaleFactor: 1
});

await page.goto('https://example.com/dashboard', {
  waitUntil: 'networkidle'
});

// Wait for application-specific readiness, not just navigation.
await page.locator('[data-testid="dashboard-ready"]').waitFor();

// Fonts can change text metrics after the first paint.
await page.evaluate(async () => {
  if (document.fonts) await document.fonts.ready;
});

await page.screenshot({
  path: 'dashboard.png',
  fullPage: true,
  animations: 'disabled'
});

await browser.close();

Use an application readiness selector when possible. networkidle can be unsuitable for pages with analytics, polling, WebSockets, or other connections that never become idle.

Reduced-motion emulation is a separate control

import { chromium } from 'playwright';

const browser = await chromium.launch();
const page = await browser.newPage({
  reducedMotion: 'reduce'
});

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

await browser.close();

reducedMotion emulates the page’s prefers-reduced-motion media feature. The documented values are 'reduce' and 'no-preference', with 'no-preference' as the default. It changes how application code may choose to animate; it is distinct from the screenshot operation’s animations option. See Playwright’s project and test options documentation.

When to use each approach

Goal Recommended setting Reason
Stable one-off page capture animations: 'disabled' Removes timing differences from CSS and Web Animations.
Stable element capture Locator screenshot with animations: 'disabled' Limits stabilization to the element you need.
Visual regression test toHaveScreenshot() Animations are disabled by default and two matching frames are required.
Document the current motion frame animations: 'allow' Preserves animation behavior, accepting timing variance.
Honor an accessibility preference reducedMotion: 'reduce' Lets the application react to prefers-reduced-motion.

Handling JavaScript-driven motion

The screenshot option covers CSS animations, CSS transitions, and Web Animations. JavaScript can still update text, canvas pixels, SVG attributes, or inline styles on a timer. For those cases, wait for a state your application exposes, then capture:

await page.locator('[data-testid="chart-rendered"]').waitFor();
await expect(page.locator('canvas')).toBeVisible();
await page.screenshot({
  path: 'chart.png',
  animations: 'disabled'
});

If a canvas is continuously redrawn, freeze the application’s data or clock in the test environment. Disabling CSS animations alone cannot make a continuously changing canvas deterministic.

Common problems and fixes

Symptom Likely cause Fix
Two screenshots differ even with animations disabled Late data, fonts, images, or JavaScript updates Wait for a readiness selector, document.fonts.ready, and required images or data.
An infinite spinner disappears Infinite animations are canceled to their initial state Assert the intended loading state or wait for loading to finish before capture.
A finite transition shows its final state Playwright fast-forwards finite animations Use animations: 'allow' if the intermediate frame is required.
Visual tests remain flaky Direct screenshots allow animations by default, or the page never reaches a stable state Use toHaveScreenshot(), explicitly disable animations, and wait for app readiness.
CSS prefers-reduced-motion rules do not apply The media feature was not emulated Create the context with reducedMotion: 'reduce'.
networkidle never completes Polling, analytics, WebSockets, or another persistent request Wait for a specific DOM readiness signal instead of global network idle.
Only a chart or video changes Canvas, video, or JavaScript updates are outside CSS animation handling Pause or mock that component, or capture a defined application state.

Performance, reliability, and cost notes

  • Disabling animations generally improves repeatability, but fast-forwarding a complex finite animation can still trigger application callbacks. Keep readiness checks explicit.
  • Full-page screenshots require layout and rasterization for the entire document. Capture a locator when a single component is enough.
  • Use a fixed viewport, device scale factor, browser version, fonts, locale, timezone, and test data for reliable diffs.
  • Do not use arbitrary sleeps as the primary synchronization method. A selector or application state is usually faster and more reliable.
  • Playwright itself has no per-screenshot service charge. Your costs come from the machines, CI minutes, browsers, storage, and any external screenshot service you add.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. It accepts a URL and returns PNG, JPEG, WebP, or PDF. Before capture, it accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers report the page verdict and billing state.

See the ScreenshotNeo API documentation for all options, including full-page capture, element selectors, custom CSS and JavaScript, waits, request blocking, device presets, dark mode, caching, signed links, asynchronous jobs, bulk capture, and PDF settings.

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

An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.

FAQ

What is the exact Playwright option?

Use animations: 'disabled' or animations: 'allow' on page and locator screenshots.

Are direct screenshots stable by default?

No. Direct page and locator screenshots default to 'allow'.

Do screenshot assertions behave differently?

Yes. Playwright Test screenshot assertions disable animations by default and wait for two consecutive matching screenshots.

Can I freeze an animation at a chosen progress point?

The documented screenshot option does not select an arbitrary frame. Use application-level controls or allow the animation and synchronize its timing yourself.

Does reduced motion replace the screenshot option?

No. reducedMotion controls the emulated media preference; animations controls screenshot capture behavior.