ScreenshotNeo

BlogHow-to

How to Capture a Screenshot of a Page with Scroll-Triggered Animations

Capture a full page or a specific scroll-triggered animation state with Playwright, Chrome DevTools, or ScreenshotNeo.

By the ScreenshotNeo team4 October 20269 min read

To capture a scroll-triggered animation, first scroll the page until the animation reaches the state you want, then capture the viewport or the relevant element. A full-page screenshot is useful for a long document, but it does not reproduce a sequence of real scroll interactions. For several animation states, scroll to each position and save a separate screenshot.

This guide uses Playwright for repeatable browser captures and Chrome DevTools for inspecting animations. The right workflow depends on whether you need one tall image, one precise animated state, or a set of comparable captures.

1. Choose what the screenshot needs to show

Goal Method What to keep in mind
Show the whole document Playwright fullPage: true It captures the scrollable page as one tall image; it does not scroll through and record each animation state.
Show one reveal, parallax position, or scroll-driven state Scroll to the triggering position, then capture the viewport or a locator The page must reach the desired visual state before the screenshot.
Compare multiple states Script a capture at each chosen scroll position Keep viewport, browser, device scale, page state, and scroll offsets consistent.
Understand how an animation behaves Chrome DevTools Animations panel Use the panel to inspect and scrub an animation; save the image separately with a screenshot command.

For a state that only appears after scrolling, use a viewport or element screenshot after scrolling. A full-page image can be useful for documenting stable content across the document, but it should not be treated as a record of the page’s actual scroll interaction.

2. Capture a scroll-triggered state with Playwright

Install Playwright and its Chromium browser in a Node.js project:

npm install playwright
npx playwright install chromium

Save this as capture-scroll-state.js. It scrolls to a chosen document position, waits for a site-specific condition when supplied, and saves the current viewport. Without a condition, it waits two animation frames so the browser can render after the scroll; this does not guarantee that an asynchronous or long-running site animation has finished.

const { chromium } = require('playwright');

(async () => {
  const url = process.argv[2] || 'https://example.com';
  const desiredY = Number(process.argv[3] || 900);
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage({ viewport: { width: 1440, height: 1000 }, deviceScaleFactor: 1 });

  try {
    await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30000 });
    await page.evaluate((y) => window.scrollTo(0, y), desiredY);
    await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));

    // Replace this with an observable condition for the page you are capturing, if available.
    // Example: await page.locator('.revealed-card').waitFor({ state: 'visible', timeout: 10000 });

    await page.screenshot({ path: 'scroll-state.png', animations: 'allow' });
    console.log('Saved scroll-state.png');
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with an optional URL and scroll position in CSS pixels:

node capture-scroll-state.js https://example.com 900

Use a site-specific signal whenever possible: wait for a class added by the reveal, a locator to become visible, an application state exposed for tests, or a known animation completion event. A fixed delay can be a fallback for a page without an observable signal, but it can be both slower and less reliable: a short delay may capture too early, while a long delay wastes time.

Capture a particular element

Once the state is active, capture only the target element. This keeps the artifact focused and avoids unrelated content around it:

const card = page.locator('.feature-card.is-visible');
await card.waitFor({ state: 'visible', timeout: 10000 });
await card.screenshot({ path: 'feature-card.png', animations: 'allow' });

Use a selector that identifies the intended element reliably. If the element is outside the viewport, scroll it into view first with await card.scrollIntoViewIfNeeded(); then wait for the desired state before capturing.

Capture a full-page image

For a single tall image of the document, use:

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

Full-page capture is not equivalent to repeatedly scrolling the page to trigger interactions. Lazy-loaded content or animations tied to actual scrolling may not be in the state you expect. If those interactions matter, script the scroll positions and save separate viewport or element images instead.

Save several states in one run

Use explicit positions and descriptive filenames for a small set of states. Keep the page and viewport fixed between captures:

const positions = [400, 900, 1400];
for (let i = 0; i < positions.length; i++) {
  await page.evaluate((y) => window.scrollTo(0, y), positions[i]);
  await page.evaluate(() => new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))));
  await page.screenshot({ path: `state-${i + 1}.png`, animations: 'allow' });
}

For production visual checks, pair each position with an observable page-specific condition. If the animation is controlled by a scroll library, use that library’s documented state or test hook where available instead of assuming that two frames are enough.

3. Understand Playwright’s animation setting

Playwright screenshot animations default to 'allow', which leaves animations untouched. That is usually the appropriate setting when capturing a live scroll-triggered state after scrolling.

Setting animations: 'disabled' does not freeze every animation at its current frame. Playwright fast-forwards finite animations to completion, firing transitionend; infinite animations are canceled to their initial state and replayed after the screenshot. Those changes can alter the state you intended to document. Use 'disabled' only when that behavior is acceptable, such as when you need a stable capture of completed finite transitions. See the Playwright screenshot API for the option details.

4. Inspect scroll-driven animations in Chrome DevTools

  1. Open the page in Chrome and open DevTools.
  2. Open the Animations panel. If it is not visible, open it from the DevTools panel menu.
  3. Trigger the animation while the panel is open, including by scrolling when the effect is scroll-driven.
  4. Select the captured animation group and scrub its timeline to inspect the progression.
  5. Return to the desired page state and take a screenshot separately, for example with Playwright or the browser’s capture command.

Chrome documents that the Animations panel can capture groups that include mouse scroll-driven animations and lets you scrub their timelines. Scrubbing is an inspection aid; it does not itself save a screenshot. See Chrome DevTools: CSS animations.

5. Make captures repeatable

  • Use a fixed viewport: viewport dimensions affect line wrapping, breakpoints, and which elements intersect the scroll trigger.
  • Keep device scale consistent: changing device scale changes output dimensions and can affect visual comparisons.
  • Use the same browser version and page state: font loading, data, cookies, and application state can change the result.
  • Choose meaningful scroll coordinates: scroll positions are document coordinates in CSS pixels; responsive layout changes can shift the element being triggered.
  • Wait for evidence: prefer a visible-state or application condition to a guessed duration.
  • Control motion when appropriate: if your page respects reduced-motion preferences, decide whether the capture should represent that mode or the default experience and keep the choice consistent.
  • Keep artifacts separate: name files by state or position so a later comparison does not confuse one state for another.

Scroll-trigger libraries may react to wheel events, intersection changes, or animation timelines. A direct window.scrollTo updates scroll position, but it may not reproduce every input event a site listens for. If direct scrolling does not trigger the effect, try scrolling through the page in increments or use the site’s own test hook. This is a practical debugging step; the right trigger depends on the page implementation.

6. Troubleshooting

Symptom Likely cause Fix
The reveal is missing from the screenshot The capture happened before the trigger or before the page updated. Scroll to the trigger, then wait for a site-specific visible-state condition or animation signal before capturing.
fullPage: true shows an unexpected animation state A full-page screenshot is a tall document capture, not a sequence of real scroll interactions. Capture the required state at its actual scroll position; save several images when several states matter.
Setting animations: 'disabled' changes the frame Finite animations are fast-forwarded and infinite ones are canceled to their initial state during capture. Use animations: 'allow' for a live state, then wait for the intended appearance before capture.
The script captures too early or is flaky A fixed timeout does not correspond to the animation’s actual completion, or the page is still loading. Wait on a selector, state change, or app signal. Use a longer navigation timeout only when the page genuinely needs it.
Direct scrolling does not trigger the effect The page may depend on incremental scroll input or a particular library trigger. Scroll in increments, inspect the trigger in DevTools, and check the page’s animation implementation or test hooks.
The target element is not found The selector is incorrect, the element has not rendered, or the page state differs. Check the selector in DevTools, wait for it to attach, and ensure the desired state makes it visible.
The screenshot differs between runs Viewport, browser, scale, fonts, data, or scroll position changed. Pin those inputs and wait for fonts or page-specific content to settle before capture.
Navigation times out The page keeps connections open or never reaches the selected load condition. Choose a navigation condition appropriate to the site, such as domcontentloaded, and separately wait for the content you need. Avoid relying on network idle for pages with continuous requests.

7. Performance, reliability, and cost

Local Playwright captures run in the browser environment you provide. A single viewport capture is generally a smaller task than a full-page image or many sequential states, but actual time and resource use depend on the page, browser, and environment; there is no universal runtime. Full-page captures may create large images, while multiple state captures multiply browser work and storage.

For reliability, reuse one browser process for a batch of captures, close pages and browsers in a finally block, and record the URL, viewport, scale, and scroll coordinate with the artifacts. Set navigation and condition timeouts deliberately. Retry only transient failures, and avoid retry loops that repeatedly capture a page whose selector or trigger is permanently wrong. Playwright itself has no per-screenshot API charge in this example; account for the compute, storage, and maintenance of the machine or CI environment running it.

8. Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. A single request returns an image or PDF, and the API accepts many parameter names used by other screenshot APIs. Its screenshot call captures the page rather than exposing a scroll-position parameter, so use it when you need a straightforward page capture; for a specific intermediate scroll-triggered state, the Playwright workflow above gives direct control over scrolling and waiting.

See the ScreenshotNeo API documentation. This cURL example saves a WebP capture:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

Equivalent Python:

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)

Equivalent Node.js:

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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));

ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before the capture; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots. Sign up for 1,000 free screenshots a month, with no card required.

9. FAQ

Can I capture the exact middle frame of a scroll animation?

Playwright’s screenshot API does not provide a universal control for selecting an animation timeline frame. Reach the desired state through the page’s scroll behavior or its own test controls, verify that state, and capture it.

Should I use a full-page screenshot for a bug report?

Use one when the bug concerns the document’s overall layout. If the issue is a particular reveal or parallax position, include a viewport capture at the scroll position that shows it; add a full-page image only if it provides useful context.

Can Chrome DevTools save the animation as an image?

The Animations panel helps inspect and scrub captured animation groups. Use a screenshot tool separately to save the page at the state you need.