How to take a full-page screenshot in Playwright
One option, fullPage in Node or full_page in Python, captures the whole page. What it misses: images that wait for a scroll, cookie bars and content inside scrolling elements. Code for both languages, tested, with the fixes.
The short version
Pass fullPage: true in Node or full_page=True in Python to page.screenshot. Playwright captures the page's full scroll height at the viewport width you set. If the page loads images on scroll, scroll it with the mouse wheel first, or those images come out empty.
import { chromium } from 'playwright';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto('https://example.com');
await page.screenshot({ path: 'full-page.png', fullPage: true });
await browser.close();from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 800})
page.goto("https://example.com")
page.screenshot(path="full-page.png", full_page=True)
browser.close()Install for Node with npm i playwright, or for Python with pip install playwright. Then fetch the browser once with npx playwright install chromium or playwright install chromium. Playwright for Node needs Node 20 or later. The same option is setFullPage(true) in Java and FullPage = true in .NET; we tested only Node and Python.
What we found in testing
We captured test pages at 1280 × 800 with the plain call above, then with the fixes on this page.
| Test page | Plain full-page call | With the fixes |
|---|---|---|
8 images with loading="lazy" | 8 of 8 loaded | 8 of 8 |
| 8 images loaded by IntersectionObserver | 0 of 8 | 8 of 8, after the wheel scroll |
| Cookie bar fixed to the bottom | Painted once, at the bottom of the first screen | Hidden with the style option |
Rows scrolling inside <main> | 1280 × 800; locator('main').screenshot() gave 1080 × 800 | 1280 × 5640, all 40 rows |
| Page 30,000 px tall | One PNG, correct top to bottom | No fix needed |
Playwright did better than Puppeteer on native lazy images (Puppeteer loaded 4 of 8 in the same test), but images loaded by JavaScript as they come into view stayed empty in both.
A script for real sites
This scrolls the page 400 px at a time with the mouse wheel, waits for the network to go quiet, returns to the top and captures with animations stopped.
import { chromium } from 'playwright';
const url = process.argv[2] ?? 'https://example.com';
const browser = await chromium.launch();
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.goto(url, { waitUntil: 'networkidle' });
// Scroll with the mouse wheel, as a visitor would, so scroll listeners and
// IntersectionObserver-based lazy loading both fire. Then go back to the top.
const height = await page.evaluate(() => document.documentElement.scrollHeight);
for (let y = 0; y < height; y += 400) {
await page.mouse.wheel(0, 400);
await page.waitForTimeout(150);
}
await page.evaluate(() => window.scrollTo(0, 0));
await page.waitForLoadState('networkidle');
await page.screenshot({ path: 'full-page.png', fullPage: true, animations: 'disabled' });
await browser.close();import sys
from playwright.sync_api import sync_playwright
url = sys.argv[1] if len(sys.argv) > 1 else "https://example.com"
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page(viewport={"width": 1280, "height": 800})
page.goto(url, wait_until="networkidle")
# Scroll with the mouse wheel so lazy images load, then go back to the top.
height = page.evaluate("document.documentElement.scrollHeight")
for _ in range(0, height, 400):
page.mouse.wheel(0, 400)
page.wait_for_timeout(150)
page.evaluate("window.scrollTo(0, 0)")
page.wait_for_load_state("networkidle")
page.screenshot(path="full-page.png", full_page=True, animations="disabled")
browser.close()Both versions turned the Wikipedia article on screenshots into a 1280 × 4487 PNG of 860 KB with every image loaded, in 3.5 seconds for Node and 3.7 for Python, browser start included.
animations: 'disabled' fast-forwards finite CSS animations and transitions to their end state and resets infinite ones, so a carousel or fade-in doesn't get caught halfway.
Hide cookie bars for the shot only
Elements with position: fixed are painted once where they sit on the first screen. A bottom cookie bar becomes a stripe across the image. Playwright's style option injects CSS for the capture only:
await page.screenshot({
path: 'full-page.png',
fullPage: true,
style: '.cookie-bar, #chat-widget { display: none !important; }',
});page.screenshot(path="full-page.png", full_page=True,
style=".cookie-bar, #chat-widget { display: none !important; }")On our test page the bar was gone from the image, and afterwards it was still on the page. That makes this safer than editing the DOM when the same page goes on to run a test. To blank out content instead of removing it, such as a live clock, use mask with a list of locators.
Scrollable elements
When an app scrolls a <main> element instead of the page, the document is one screen tall and a full-page shot returns 1280 × 800. A locator screenshot doesn't help either: it captures the box you see. Let the container grow, then capture the page:
await page.addStyleTag({
content: 'html, body { height: auto !important; } main { overflow: visible !important; height: auto !important; }',
});
await page.screenshot({ path: 'panel.png', fullPage: true });page.add_style_tag(content="html, body { height: auto !important; } main { overflow: visible !important; height: auto !important; }")
page.screenshot(path="panel.png", full_page=True)Our test page went from 1280 × 800 to 1280 × 5640 in both languages. Swap main for the element that has the scroll bar.
One region of a long page
Unlike Puppeteer, Playwright accepts a clip together with fullPage, and measures the clip from the top of the whole page:
await page.screenshot({ path: 'region.png', fullPage: true, clip: { x: 0, y: 2000, width: 1280, height: 600 } });WebP, and no browser to run
Playwright can't write WebP, and a long PNG is heavy: 860 KB for that Wikipedia page. The Shotkiln API returns WebP and runs the browser for you:
import os, requests
r = requests.get("https://shotkiln.com/v1/screenshot",
params={"url": "https://en.wikipedia.org/wiki/Screenshot", "full_page": "true", "format": "webp"},
headers={"x-api-key": os.environ["SHOTKILN_KEY"]}, timeout=60)
r.raise_for_status()
open("full-page.webp", "wb").write(r.content)
print(r.headers["x-cache"], r.headers.get("x-render-ms"), len(r.content))Our run printed MISS 5058 407642: 5.1 seconds of render time and a 408 KB WebP at the same 1280 × 4487, with the footer logos that only load on scroll. Like the script above, the API scrolls the page before a full-page capture; pass "scroll": "false" to skip that. Within the cache time, one hour by default, a repeat request prints HIT None with the same bytes, straight from cache. Keep Playwright for anything behind a login.