Tutorial · Node.js & Python

Playwright screenshots in Node.js and Python

Playwright drives Chromium, Firefox and WebKit from one API. Here is the screenshot code you need, tested with Playwright 1.64, and the details that make full-page captures come out right.

Updated October 29, 2026 · 9 min read

To take a Playwright screenshot, open a page and call page.screenshot({ path: "page.png" }). Add fullPage: true to capture the whole scrollable page, or call screenshot() on a locator to capture one element. The same API works in Node.js, Python, Java and .NET, with Chromium, Firefox and WebKit.

The examples below were run with Playwright 1.64 (Chromium and WebKit) on Node.js 26 and Python 3.11, against example.com and a local test page with lazy-loaded images, a cookie banner and a chat widget.

Install Playwright

Node.js (20 or later)Python (3.10 or later)
Librarynpm install playwrightpip install playwright
Browsernpx playwright install chromiumplaywright install chromium
On Linux serversadd --with-deps for the system librariesadd --with-deps

playwright install without a name downloads Chromium, Firefox and WebKit. The browsers are Playwright’s own builds, matched to the library version, so run the install command again after every upgrade.

1. Take a screenshot in Node.js

// screenshot.mjs
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");
  await page.screenshot({ path: "example.png" }); // the viewport, 1280 × 800
  await page.screenshot({ path: "example-full.png", fullPage: true }); // the whole page
} finally {
  await browser.close();
}

page.goto waits for the load event by default. page.screenshot returns the image as a Buffer too, so you can upload it instead of writing a file: const png = await page.screenshot();.

2. Take a screenshot in Python

The sync API reads like the Node.js version:

# screenshot.py
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="example.png", full_page=True)
    page.locator("#pricing").screenshot(path="pricing.png")
    browser.close()

For many URLs, the async API captures them in parallel in one browser. A semaphore keeps the number of open pages, and with it the memory, in check:

# many.py — several pages at once with the async API
import asyncio

from playwright.async_api import async_playwright

URLS = ["https://example.com", "https://example.org", "https://example.net"]


async def main():
    async with async_playwright() as p:
        browser = await p.chromium.launch()
        semaphore = asyncio.Semaphore(4)  # at most 4 pages open at once

        async def shoot(i, url):
            async with semaphore:
                page = await browser.new_page(viewport={"width": 1280, "height": 800})
                try:
                    await page.goto(url, timeout=30_000)
                    await page.screenshot(path=f"shot-{i}.png", full_page=True)
                finally:
                    await page.close()

        results = await asyncio.gather(*(shoot(i, u) for i, u in enumerate(URLS)), return_exceptions=True)
        for url, result in zip(URLS, results):
            print(url, "failed:" if result else "ok", result or "")
        await browser.close()


asyncio.run(main())

return_exceptions=True keeps one slow or broken site from cancelling the others.

3. Full-page screenshots with lazy-loaded images

fullPage: true captures the whole page, but only as it is at that moment. On our test page, a plain full-page capture showed three problems:

The fix: scroll through the page, wait for the images, hide what doesn’t belong in the picture and stop animations:

// full-page.mjs
import { chromium } from "playwright";

// Scroll down one screen at a time so lazy-loaded images start loading, then back to the top
async function scrollThrough(page) {
  await page.evaluate(async () => {
    for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) {
      window.scrollTo(0, y);
      await new Promise((resolve) => setTimeout(resolve, 100));
    }
    window.scrollTo(0, 0);
  });
}

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 scrollThrough(page);
  // wait until every image has loaded (or failed)
  await page.waitForFunction(() => [...document.images].every((img) => img.complete));
  await page.screenshot({
    path: "full.png",
    fullPage: true,
    animations: "disabled", // finite animations jump to their end, infinite ones to their start
    style: "#cookie-banner, .chat-launcher { display: none !important; }",
  });
} finally {
  await browser.close();
}

The style option injects CSS for the screenshot only. Replace the selectors with the banner and widget IDs of the site you capture; every site uses different ones. With this script, all ten lazy images on our test page loaded and the banner was gone.

4. Screenshot one element or a region

await page.locator("#pricing").screenshot({ path: "pricing.png" });

await page.screenshot({
  path: "top.png",
  clip: { x: 0, y: 0, width: 1280, height: 400 }, // a region in CSS pixels
});

await page.screenshot({
  path: "masked.png",
  mask: [page.locator(".chat-launcher"), page.locator("#ua")], // painted over in pink
});

A locator screenshot waits for the element, scrolls it into view and crops to its box. mask covers changing content, such as dates or ads, with a solid box (maskColor sets the color), which keeps comparisons stable.

5. Mobile devices and retina screenshots

Playwright ships device profiles with viewport, device scale factor, touch support and user agent:

// mobile.mjs
import { webkit, chromium, devices } from "playwright";

const browser = await webkit.launch(); // iPhone profiles default to WebKit, Safari's engine
const iphone = await browser.newContext({ ...devices["iPhone 15"] }); // 393 × 659, scale 3, touch, Safari UA
const page = await iphone.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "iphone-15.png" });
await browser.close();

// A retina desktop screenshot: same layout, twice the pixels
const chrome = await chromium.launch();
const retina = await chrome.newPage({ viewport: { width: 1280, height: 800 }, deviceScaleFactor: 2 });
await retina.goto("https://example.com");
await retina.screenshot({ path: "retina.png" }); // 2560 × 1600 px
await chrome.close();

The iPhone 15 screenshot comes out at 1179 × 1977 pixels: 393 × 659 CSS pixels at scale 3. Pass scale: "css" to screenshot() to get one image pixel per CSS pixel instead. Object.keys(devices) lists all profiles (iPhone 17, Pixel 9, iPad Pro 11 and more); our viewport sizes pages list the dimensions of common devices.

6. Dark mode, language and time zone

These are options of the browser context, so every page in it shares them:

// dark-mode.mjs
import { chromium } from "playwright";

const browser = await chromium.launch();
const context = await browser.newContext({
  viewport: { width: 1280, height: 800 },
  colorScheme: "dark", // prefers-color-scheme: dark
  reducedMotion: "reduce",
  locale: "de-DE", // navigator.language and Accept-Language
  timezoneId: "Europe/Berlin",
});
const page = await context.newPage();
await page.goto("https://example.com");
await page.screenshot({ path: "dark-de.png" });
await browser.close();

Playwright renders in light mode unless you set colorScheme, whatever your operating system uses. The geolocation option only answers the JavaScript geolocation API; it doesn’t change the IP address, so sites that localize by IP still see your server’s country.

7. Screenshot options at a glance

OptionWhat it does
pathFile to write; the extension sets the format if type is missing
fullPageThe whole scrollable page instead of the viewport
clipA region { x, y, width, height } in CSS pixels
type, quality"png" (default), "jpeg" or "webp"; quality 0–100 for JPEG and WebP
omitBackgroundTransparent instead of white where the page has no background (PNG, WebP)
animations"disabled" stops CSS animations and transitions
caret"hide" (default) or "initial" for the text cursor
mask, maskColorCover locators with a solid box
styleCSS applied only while the screenshot is taken
scale"device" (default) or "css" pixels
timeoutMaximum time in milliseconds; 0 means no limit

8. The same page in Chromium, Firefox and WebKit

Rendering differs between engines, especially fonts, form controls and newer CSS. One loop captures all three:

// all-browsers.mjs
import { chromium, firefox, webkit } from "playwright";

for (const browserType of [chromium, firefox, webkit]) {
  const browser = await browserType.launch();
  try {
    const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
    await page.goto("https://example.com");
    await page.screenshot({ path: `example-${browserType.name()}.png`, fullPage: true });
  } finally {
    await browser.close();
  }
}

This needs all three browsers installed (npx playwright install). page.pdf() works in Chromium only.

9. Visual regression tests with toHaveScreenshot

Playwright Test (npm install -D @playwright/test) compares screenshots against a stored baseline:

// tests/home.spec.js
import { test, expect } from "@playwright/test";

test("home page looks the same", async ({ page }) => {
  await page.goto("https://example.com");
  await expect(page).toHaveScreenshot("home.png", {
    fullPage: true,
    maxDiffPixelRatio: 0.01, // allow 1 % of the pixels to differ
    mask: [page.locator(".chat-launcher")], // parts that change on every load
  });
});

Run npx playwright test --update-snapshots once to write the baseline (it lands in tests/home.spec.js-snapshots/, with the operating system in the file name), then npx playwright test on every change. toHaveScreenshot disables animations by default and retakes the screenshot until two in a row match, which removes most flakiness. Fonts render differently on macOS, Windows and Linux, so create baselines on the same system your CI runs on, for example Playwright’s Docker image.

10. Playwright or a screenshot API?

Running Playwright yourself is the right choice for tests, for flows that log in and click through several pages, and when you need Firefox or WebKit. For screenshots as a feature of your product (link previews, archives, reports, monitoring), you also run the browsers: memory per page, crashed processes, fonts and emoji on Linux servers, cookie banners per site and country, queues and timeouts.

A screenshot API does the rendering for you. With screenshotbase, the full-page capture from section 3 is one request:

curl -G https://api.screenshotbase.com/v1/take \
  -H "apikey: $SCREENSHOTBASE_KEY" \
  --data-urlencode "url=https://example.com" \
  -d full_page=1 \
  -d dark_mode=1 \
  -d block_cookie_banners=1 \
  -o example.png

full_page scrolls the page first so lazy images load and draws bars fixed to the bottom at the end of the page. From Python, with an iPhone profile:

# api_screenshot.py — no browser on your side
import os

import requests

resp = requests.get(
    "https://api.screenshotbase.com/v1/take",
    params={
        "url": "https://example.com",
        "full_page": 1,
        "viewport_device": "iphone-15-pro",
        "dark_mode": 1,
        "block_cookie_banners": 1,
    },
    headers={"apikey": os.environ["SCREENSHOTBASE_KEY"]},
    timeout=120,  # a render may take up to 110 seconds
)
if not resp.ok:
    error = resp.json().get("error", {})
    raise RuntimeError(f"screenshotbase {resp.status_code} {error.get('code')}: {error.get('message')}")
with open("iphone-dark.png", "wb") as f:
    f.write(resp.content)

How the Playwright options map to /v1/take parameters:

Playwrightscreenshotbase /v1/take
fullPage: truefull_page=1 (scrolls first, for lazy images)
viewport, deviceScaleFactorviewport_width, viewport_height, device_scale_factor
devices["iPhone 15"]viewport_device=iphone-15
colorScheme: "dark", reducedMotiondark_mode=1, reduce_motion=1
locale, timezoneIdlocale, time_zone
locator("#pricing").screenshot()selector=#pricing
clipclip_x, clip_y, clip_width, clip_height
type, quality, omitBackgroundformat (png, jpg, webp, avif, gif, svg), quality, omit_background=1
style, hiding elementsstyles, hide_selectors, block_cookie_banners, block_chats, block_ads
waitUntil, waitForSelectorwait_until, wait_for_selector, delay
extraHTTPHeaders, cookies, httpCredentialsheaders, cookies, authorization
proxyproxy, or ip_country_code for an exit IP in one of 117 countries
page.pdf()format=pdf
no equivalentformat=markdown for the page’s text as markdown

What the API doesn’t do: it renders with Chromium only, so cross-browser checks stay with Playwright. It runs up to 20 typed actions and your own script before the capture, but it isn’t a place for long multi-page flows, and it doesn’t solve CAPTCHAs; with ignore_host_errors=0 a bot-challenge page fails the request instead of being captured.

The free plan has 300 screenshots a month for testing and personal projects; paid plans start at $15 a month for 3,000 (pricing). The Playwright alternative for screenshots page compares both approaches with sources, and the website screenshot tool and full-page screenshot tool try the API without code. To see a page as visitors in another country do, use the screenshot from another country tool.

Frequently asked questions

How do I take a full-page screenshot with Playwright?

Call page.screenshot({ path: 'page.png', fullPage: true }) in Node.js or page.screenshot(path='page.png', full_page=True) in Python. Scroll through the page first if it lazy-loads images, otherwise they stay empty in the capture.

How do I screenshot a single element?

Use a locator: await page.locator('#pricing').screenshot({ path: 'pricing.png' }). Playwright waits for the element, scrolls it into view and captures only its box.

Can Playwright take screenshots in Safari?

It takes screenshots with WebKit, the engine behind Safari, on Linux, macOS and Windows. The rendering is close to Safari but not identical, since it's Playwright's own WebKit build, not the Safari app.

How do I compare screenshots in tests?

Use Playwright Test's await expect(page).toHaveScreenshot('home.png'). The first run with --update-snapshots stores a baseline; later runs fail when the page differs by more than the threshold you set, and attach a diff image to the report.

Does Playwright's geolocation option change the country a site sees?

No. geolocation only answers the JavaScript geolocation API. Sites that pick prices, language or cookie banners by IP address need a proxy in that country, for example the screenshot API's ip_country_code.

Screenshot API

Take screenshots from your code

screenshotbase renders any website in a real browser and returns the image: full page or viewport, any device size, retina, from 117 countries (paid plans), with cookie banners and ads hidden.

/v1/take documentation Screenshot API All documentation

300 free screenshots every month. No credit card required.

GET https://api.screenshotbase.com/v1/take?url=https://example.com&full_page=true

# Response: the image (PNG, JPG, WebP, AVIF or GIF)
curl -G "https://api.screenshotbase.com/v1/take" \
  --data-urlencode "url=https://example.com" \
  -d viewport_width=1280 -d viewport_height=800 \
  -d full_page=true -d format=png \
  -H "apikey: YOUR-API-KEY" \
  -o screenshot.png

Keep reading