ScreenshotNeo

BlogHow-to

How to Take a Playwright Screenshot After Scrolling to the Bottom of a Page

Scroll to trigger lazy content, then capture the full page with Playwright. Includes document and container patterns, troubleshooting, and a no-browser API option.

By the ScreenshotNeo team4 October 20267 min read

Direct answer: If the page content is already loaded, capture the entire document with await page.screenshot({ path: 'full-page.png', fullPage: true }). If the site loads content as you scroll, scroll through the page first to trigger that loading, wait for a meaningful completion condition, and then take the full-page screenshot. fullPage: true controls screenshot size; it does not itself make scroll-triggered content load.

1. Set up a runnable Playwright script

Install Playwright and its browser, then save this as screenshot.mjs. Replace the example URL and, if needed, the loading condition with one that matches the page.

npm init -y
npm install playwright
npx playwright install chromium
import { chromium } from 'playwright';

const url = 'https://example.com';
const browser = await chromium.launch({ headless: true });
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });

try {
  await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 30_000 });

  // For a static page, this may be all the preparation you need.
  // For scroll-triggered content, use the scrolling pattern below first.
  await page.screenshot({ path: 'full-page.png', fullPage: true });
} finally {
  await browser.close();
}

Run it with node screenshot.mjs. The Page API defaults fullPage to false, which captures only the viewport. Set it to true for the full scrollable page. See the Playwright Page screenshot API.

2. Scroll before capturing when content loads on demand

Infinite lists, lazy images, and other pages may add or reveal content only after the user scrolls. A full-page screenshot is not a substitute for triggering that behavior. Scroll to the bottom, let the site finish loading, and then capture.

Scroll the document in steps

This pattern advances the main document by roughly one viewport at a time. It stops when the document height no longer grows after a scroll. The maximum-step guard prevents an endless loop on pages that keep appending items. Choose a meaningful wait or content condition for your site; a fixed delay alone cannot guarantee that an asynchronous request has completed.

async function scrollDocumentToLoad(page, { maxSteps = 30, settleMs = 500 } = {}) {
  let previousHeight = 0;
  let stableSteps = 0;

  for (let step = 0; step < maxSteps; step++) {
    const height = await page.evaluate(() => document.documentElement.scrollHeight);
    await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
    await page.waitForTimeout(settleMs);

    const newHeight = await page.evaluate(() => document.documentElement.scrollHeight);
    if (newHeight === height && newHeight === previousHeight) {
      stableSteps++;
      if (stableSteps >= 2) break;
    } else {
      stableSteps = 0;
    }
    previousHeight = newHeight;
  }

  // Leave the document at the top before a full-page capture.
  await page.evaluate(() => window.scrollTo(0, 0));
}

await page.goto('https://example.com/feed', { waitUntil: 'domcontentloaded' });
await scrollDocumentToLoad(page);
await page.screenshot({ path: 'feed.png', fullPage: true });

This is a starting pattern, not a universal infinite-scroll detector: some sites keep a constant document height while replacing items, load only after a particular sentinel enters view, or require multiple requests. In those cases, use the site’s observable signal, such as a known final item, a loading indicator disappearing, or an expected item count.

Scroll to a known target

If the page has a footer or a known final item, scrolling that locator into view can trigger the same lazy-loading behavior. Playwright’s guide uses this approach as one way to force an infinite list to load more elements.

await page.getByText('End of results').scrollIntoViewIfNeeded();
await page.getByTestId('final-item').waitFor({ state: 'visible', timeout: 10_000 });
await page.screenshot({ path: 'results.png', fullPage: true });

Use a locator that actually appears when loading is complete. If the target is absent until more items load, scroll incrementally or use a sentinel locator that triggers the next batch.

Use wheel input or a nested scroll container

When the page responds to real wheel input, move the pointer over the active scrolling area before sending wheel events. For an element with its own scrollbar, update that element’s scrollTop instead of the document’s position.

// Wheel the main page. Repeat as needed while observing a loading signal.
await page.mouse.move(700, 700);
await page.mouse.wheel(0, 800);
await page.waitForTimeout(500);

// Scroll a nested container directly.
const feed = page.getByTestId('scrolling-container');
await feed.evaluate(element => { element.scrollTop = element.scrollHeight; });
await page.waitForTimeout(500);
await page.screenshot({ path: 'feed-container.png', fullPage: true });

For nested feeds, a page-level full-page screenshot captures the document’s scrollable page, not necessarily every item hidden inside an independently scrollable element. A locator screenshot of that element captures only its currently scrolled content. To capture all feed items, trigger their loading, then either make the container expand to its full content for capture or capture the loaded items using a page-level layout that exposes them.

3. Choose the screenshot scope

Goal Approach What to keep in mind
Entire document page.screenshot({ fullPage: true }) Scroll-triggered content still needs to be loaded first.
Visible viewport page.screenshot() or fullPage: false Capture the current scroll position only.
One component locator.screenshot() A scrollable element shows its currently visible scrolled content. See the ElementHandle screenshot documentation; Playwright recommends locator-based screenshots over ElementHandle screenshots.

4. Synchronize on page state, not guesswork

Navigation completion and content completion are different. domcontentloaded tells you the initial document has been parsed, but a client-side feed may still be fetching or rendering items. Prefer waiting for a selector, known item, or loading state that represents the content you need. Playwright’s scrolling guide notes that most actions scroll automatically, while explicit scrolling is useful when you need to trigger loading or position the page for a screenshot: Actions: Scrolling.

For pages where image loading matters, wait for the relevant image elements to finish loading or for a site-specific ready signal before capture. Do not assume that reaching the page bottom means every image, network request, or client-side render has completed.

5. Troubleshooting

Symptom Likely cause Fix
The image contains only the first screen. fullPage was omitted or false. Use page.screenshot({ path: 'full-page.png', fullPage: true }).
The image is full height but later feed items are missing. The site loads items only after scrolling, and capture happened before they were added. Scroll to trigger loading and wait for a specific item, sentinel, or loading state before taking the screenshot.
The scroll loop never ends. The page continuously appends content or height changes due to ads, animations, or layout shifts. Set a maximum number of steps and stop based on an expected item count or site-specific completion marker.
Scrolling does not move the feed. The active scroller is a nested container rather than the document, or the pointer is over the wrong region for wheel input. Hover the feed before mouse.wheel(), or update that container’s scrollTop through a locator.
A component screenshot omits most of a scrollable panel. Element screenshots show the current scrolled portion of a scrollable element. Load all needed content and adjust the layout or capture the intended items another way.
Screenshot differs between local and CI. OS, browser version, fonts, settings, hardware, or headless mode can change rendering. Keep visual baselines and comparisons in the same environment. See Playwright’s visual comparison guidance.
Navigation or selector wait times out. The URL is unreachable, the selector changed, or the expected content never appears. Check the URL and locator, inspect page errors and response status, and wait for the condition that actually signals readiness.

6. Performance, reliability, and cost

Scrolling and waiting add time to each capture, so stop when the content you need is present rather than repeatedly traversing a page without a completion condition. Full-page images can also become very tall on long feeds; if the downstream task needs only a region or component, capture that scope instead. A page that continually loads more items needs an explicit stopping rule to bound both runtime and output size.

For repeatable screenshots, pin the Playwright and browser versions and use the same operating environment for capture and visual comparison. Playwright warns that rendering can vary across operating systems, software versions, settings, hardware, power source, and headless mode. The code above runs the browser locally, so cost depends on the compute and runtime environment you provide; this method does not include a hosted screenshot service.

Or skip the browser setup

For a one-call screenshot API, ScreenshotNeo accepts a URL and returns an image or PDF. Its API documentation describes the options. For a basic capture:

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

ScreenshotNeo removes cookie banners, popups, and chat widgets 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; paid plans start at $5 for 3,000. Sign up for the free plan.

FAQ

Does Playwright scroll to the bottom automatically for a full-page screenshot?

fullPage: true captures the full scrollable page, but it does not guarantee that scroll-triggered content has been activated and loaded. Scroll first when the site requires it.

Should I use a fixed delay after each scroll?

A short delay can give a page time to react, but it is not a reliable completion check by itself. Wait for a page-specific content or loading condition whenever possible.

Can I capture a page that has an infinite feed?

Yes, if you define when to stop, such as a target item count or an end marker. Without a stopping condition, the page may keep adding content.

Why does a full-page screenshot differ from manually stitching viewport screenshots?

They are different capture approaches. If exact visual consistency matters, keep the browser environment stable and compare captures made in that same environment.