How to Capture a Scrolling Web Page with Playwright
Use Playwright’s fullPage screenshot option to capture content below the fold, with practical guidance for lazy loading, visual tests, and troubleshooting.
To capture a scrolling web page with Playwright, navigate to it and set fullPage: true:
await page.goto('https://example.com');
await page.screenshot({ path: 'screenshot.png', fullPage: true });
By default, page.screenshot() captures only the visible viewport. Playwright describes fullPage: true as capturing the full scrollable page instead. See the Page API and the screenshots guide.
Set up a runnable Playwright script
The example below uses Playwright’s JavaScript library with Chromium. It opens a page, waits for navigation to finish, and writes a full-page PNG. Choose a real target URL when you run it.
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('https://example.com', { waitUntil: 'load' });
await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
await browser.close();
}
Install the package and browser if needed:
npm install playwright
npx playwright install chromium
For a project already using Playwright Test, use its configured browser and page fixture instead of launching another browser:
import { test, expect } from '@playwright/test';
test('captures the full page', async ({ page }) => {
await page.goto('https://example.com');
await page.screenshot({ path: 'artifacts/page.png', fullPage: true });
});
Capture pages with content below the fold
Full-page capture asks Playwright to include the full scrollable page, not just what is currently visible. It does not promise to trigger every website’s scroll-driven loading behavior. A page that loads images, cards, or more rows only when the user scrolls may need a site-specific preparation step before capture.
If content appears only after scrolling, scroll in increments and allow the page’s own loading mechanism to run. The following helper is an example for pages whose content is appended as the document grows; it has a maximum number of passes to avoid looping forever:
async function scrollToLoadMore(page, maxPasses = 20) {
let previousHeight = 0;
for (let pass = 0; pass < maxPasses; pass++) {
const height = await page.evaluate(() => document.documentElement.scrollHeight);
if (height === previousHeight) break;
previousHeight = height;
await page.evaluate(() => window.scrollTo(0, document.documentElement.scrollHeight));
await page.waitForTimeout(400); // Replace with a page-specific readiness condition when possible.
}
await page.evaluate(() => window.scrollTo(0, 0));
}
await page.goto('https://example.com');
await scrollToLoadMore(page);
await page.screenshot({ path: 'full.png', fullPage: true });
This is only a general pattern. Some sites use virtualized lists that remove off-screen rows, a “Load more” button, or different loading signals. For those pages, interact with the relevant control and wait for a locator or response that indicates the required content is present. Inspect the saved image to confirm that the desired content was included.
Choose the right screenshot scope
| Goal | Method | Behavior |
|---|---|---|
| Capture the page beyond the viewport | page.screenshot({ fullPage: true }) |
Captures the full scrollable page. |
| Capture only what is currently visible | page.screenshot() |
Uses the viewport; fullPage defaults to false. |
| Capture one element | await page.locator('main').screenshot({ path: 'main.png' }) |
Clips the image to the element’s size and position. |
| Capture a specified rectangle | page.screenshot({ clip: { x, y, width, height } }) |
Captures the requested page area. |
For an element screenshot, prefer locator-based locator.screenshot(). A screenshot of a scrollable element shows only the content currently scrolled into view inside that element; it is not a way to capture every item in an internally scrolling panel. See the ElementHandle API for the documented limitation.
Save an image or work with its bytes
Set path to write the screenshot to disk. Playwright infers the image type from the path extension. If you omit path, the method returns a buffer, useful for passing the bytes to another library or image comparison step:
const imageBytes = await page.screenshot({ fullPage: true });
// imageBytes is a Buffer in Node.js and can be passed to an image-processing step.
Use a clear extension such as .png, .jpeg, or .webp that matches your intended output. Check the current Page API for supported formats and option details.
Screenshot options that affect the result
| Option | When to use it | Notes |
|---|---|---|
fullPage |
Include the page below the viewport. | Defaults to false. |
path |
Write the image to disk. | The extension determines the image type. |
scale |
Choose output pixel density. | css produces one image pixel per CSS pixel; device uses device pixels and can produce larger images. |
type and quality |
Choose a supported image format and lossy quality where applicable. | JPEG quality defaults to 80. PNG does not use the quality option. |
animations |
Reduce motion-related differences in captures. | Disabling animations changes how finite and infinite animations are handled. Use it when that changed state is appropriate for the capture. |
mask and maskColor |
Cover regions with variable content in a screenshot or test. | The documented default mask color is pink. |
clip |
Capture a rectangular portion of the page. | Specify the rectangle’s coordinates and dimensions. |
omitBackground |
Request a transparent background where supported. | Does not apply to JPEG. |
Review the current Page API for the complete option schema for your installed Playwright version. For general page captures, PNG is a practical lossless default. Choose CSS scale when you want output dimensions to track CSS pixels; choose device scale when device-pixel detail matters and larger files are acceptable.
Use full-page screenshots in visual tests
A saved screenshot is an artifact for inspection or processing. For a repeatable Playwright Test visual assertion, use toHaveScreenshot():
import { test, expect } from '@playwright/test';
test('homepage visual baseline', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png', { fullPage: true });
});
The assertion waits for two consecutive page screenshots to match and compares the last screenshot with the expectation. Review the visual comparisons guide and PageAssertions API. Screenshot output can vary with the operating system, browser version, browser settings, hardware, power source, headless mode, and other environment details. Keep those conditions consistent for baselines, and review intentional visual changes.
Or skip the browser setup
ScreenshotNeo is a website screenshot API: send one GET request with a URL and receive an image or PDF. Its API accepts a page URL and can return PNG, JPEG, or WebP. 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,
)
r.raise_for_status()
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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for AI agents. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots.
Sign up free for 1,000 screenshots a month, with no card required.
Troubleshooting
| Symptom | Likely cause | What to do |
|---|---|---|
| The image contains only the first screen. | fullPage was omitted or set to false. |
Pass fullPage: true to page.screenshot(). |
| Images or cards below the fold are missing. | The page loads them only after scrolling or another interaction. | Trigger the site’s actual loading behavior, wait for a meaningful page-specific condition, then capture and inspect the result. |
| A long list is still incomplete after scrolling. | The list may be virtualized, button-driven, or loaded through a site-specific mechanism. | Use the relevant control or loading signal. A generic scroll loop cannot guarantee every site’s content will load. |
| The screenshot changes between runs. | Animations, dynamic content, fonts, or different browser environments affect rendering. | Stabilize the page state, consider animation handling or masks, and run visual comparisons in a consistent environment. |
| The output is unexpectedly large. | A long page and device-pixel scale can produce many image pixels. | Consider scale: 'css' or a lossy format where suitable; verify that readability and test needs are still met. |
| A panel screenshot omits rows. | The panel itself is scrollable, so only its currently scrolled content is captured. | Scroll the panel and capture the relevant state, or use the page’s own mechanism to make the required content available. |
| The transparent background option has no effect. | The selected format may be JPEG. | Use a supported format with transparency; omitBackground does not apply to JPEG. |
Performance, reliability, and cost considerations
A full-page image can contain far more pixels than a viewport image, especially for long documents or device-scale output. That affects image size, memory use, transfer time, and the time needed to inspect or compare the result. Capture only the scope you need, use CSS scale when device pixels are unnecessary, and choose a compressed format when the workflow permits it.
For reliable captures, wait for the content relevant to your task rather than assuming navigation alone means every asynchronous asset is ready. Prefer a page-specific selector, application signal, or known response over an arbitrary delay. Confirm lazy-loaded and interactive content in the output. For regression tests, keep the browser and host environment consistent and review baseline changes rather than treating every pixel difference as a product change.
Playwright is a browser automation library you run in your own environment, so account for browser installation, execution time, storage, and any CI resources your workflow uses. ScreenshotNeo offers usage tiers for API captures: Free includes 1,000 per month; Starter is $5 for 3,000; Growth is $15 for 15,000; Pro is $39 for 60,000; Scale is $99 for 250,000; Business is $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. See its documentation for API configuration and response details.
FAQ
Does full-page capture scroll the browser visibly?
The API option is documented as capturing the full scrollable page. The documentation does not promise that it triggers every site’s scroll-based loading behavior, so prepare and verify pages that load content on demand.
Can I use the screenshot without saving a file?
Yes. Omit path and use the returned buffer for processing or comparison.
Should I use a page screenshot or a locator screenshot?
Use a page screenshot for the page or viewport and a locator screenshot when the output should be clipped to one element.
Can I capture a full web page as a PDF with Playwright?
This guide covers page screenshots. PDF generation is a separate browser output workflow; consult the current Playwright documentation for the browser and page PDF APIs supported by your setup.


