Keyword: playwright screenshot · Updated October 15, 2026

Playwright Screenshot: Every Option Explained (Full Page, Element, Scrollable Div) + Copy-Paste Code

The one-line version of a Playwright screenshot fits in a tweet. The trouble starts afterwards: the full-page capture stops at the fold of a chat panel, the image is twice the size you expected, a carousel moves between runs, or a cookie banner covers the button you wanted to show. This page lists every option of page.screenshot() and locator.screenshot() with TypeScript and Python examples, and gives a working fix for scrollable containers.

The basic screenshot

TypeScript / JavaScript:

import { chromium } from "playwright"; const browser = await chromium.launch(); const page = await browser.newPage({ viewport: { width: 1440, height: 900 } }); await page.goto("https://example.com"); await page.screenshot({ path: "example.png" }); await browser.close();

Python (sync API):

from playwright.sync_api import sync_playwright with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page(viewport={"width": 1440, "height": 900}) page.goto("https://example.com") page.screenshot(path="example.png") browser.close()

Without options you get the visible viewport as a PNG. The format comes from the file extension, so example.jpeg or example.webp switch formats without any other change.

Every page.screenshot() option

Python uses snake_case for the same options (full_page, omit_background, mask_color). Defaults are from the current Playwright API reference.

Option (TS / Python)DefaultUse it for
pathnoneFile to write. Without it, nothing is saved and you only get the buffer.
typepngpng, jpeg or webp.
qualityjpeg 80, webp 1000 to 100. Ignored for PNG. WebP at 100 is lossless.
fullPage / full_pagefalseCapture the whole scrollable document.
clipnone{ x, y, width, height } region of the page.
scale"device""css" gives one pixel per CSS pixel and smaller files on high-DPI contexts.
animations"allow""disabled" fast-forwards finite animations and resets infinite ones.
caret"hide"Hides the blinking text cursor. Set "initial" to keep it.
masknoneArray of locators to cover with a solid box (timestamps, avatars, ads).
maskColor / mask_color#FF00FFColor of the mask boxes. Added in v1.35.
stylenoneCSS applied only while the screenshot is taken. Pierces Shadow DOM and iframes. Added in v1.41.
omitBackground / omit_backgroundfalseTransparent background instead of white. Not for JPEG.
timeoutsee docsMax time in ms. Configurable globally with page.setDefaultTimeout() or actionTimeout.
signalnoneAn AbortSignal to cancel the capture. Added in v1.62.

locator.screenshot() accepts the same set except fullPage and clip, because the element defines the area.

Full page screenshot

// TypeScript await page.goto("https://example.com/blog", { waitUntil: "load" }); await page.screenshot({ path: "blog-full.png", fullPage: true }); # Python page.goto("https://example.com/blog", wait_until="load") page.screenshot(path="blog-full.png", full_page=True)

fullPage captures the document as if the window were tall enough to fit it. It does not scroll the page first, so images with loading="lazy" below the fold may still be empty. Scroll before capturing:

// Trigger lazy-loaded images and infinite sections, then return to the top await page.evaluate(async () => { for (let y = 0; y < document.body.scrollHeight; y += window.innerHeight) { window.scrollTo(0, y); await new Promise((r) => setTimeout(r, 150)); } window.scrollTo(0, 0); }); await page.waitForLoadState("networkidle"); await page.screenshot({ path: "blog-full.png", fullPage: true });
Playwright's docs discourage networkidle for tests and recommend web-first assertions instead. For a one-off capture script it is still a reasonable "network has calmed down" signal. In tests, wait for a specific element with await expect(locator).toBeVisible().

Screenshot of one element

// TypeScript await page.getByTestId("pricing-table").screenshot({ path: "pricing.png" }); // With a CSS selector and disabled animations await page.locator(".hero").screenshot({ path: "hero.png", animations: "disabled" }); # Python page.get_by_test_id("pricing-table").screenshot(path="pricing.png")

The locator waits for the element to be visible and scrolls it into view. If the locator matches several elements, Playwright throws a strict mode error instead of guessing. Narrow it with .first() or a better selector.

Screenshot of a scrollable element (the part most guides skip)

Searches for "playwright screenshot scrollable element" exist because fullPage only expands the document. A chat log, a data grid or a sidebar with its own overflow: auto stays at its fixed height, and you get the visible slice. There are three ways around it.

Option A: remove the height limit before capturing

Add a stylesheet that lets the container grow to its content, then capture it. This works for most layouts.

const grid = page.locator(".data-grid__body"); await page.addStyleTag({ content: ` .data-grid__body { height: auto !important; max-height: none !important; overflow: visible !important; } html, body { height: auto !important; overflow: visible !important; } `, }); await grid.screenshot({ path: "grid-all-rows.png" });

Virtualized lists (react-window, AG Grid with row virtualization) only render visible rows, so growing the container shows empty space. For those, use option C or turn virtualization off in a test build.

Option B: full page with an app shell

Many dashboards set html, body { height: 100% } and scroll inside main. Then fullPage: true returns exactly one viewport. Apply the same CSS idea to the shell and take a full-page shot:

await page.addStyleTag({ content: ` html, body, #root, main { height: auto !important; overflow: visible !important; } `, }); await page.screenshot({ path: "dashboard-full.png", fullPage: true });

Option C: capture in steps

Scroll the container one height at a time and save each slice. You can stitch them with an image library afterwards or keep them as a numbered set for review.

const box = page.locator(".chat-log"); const { scrollHeight, clientHeight } = await box.evaluate((el) => ({ scrollHeight: el.scrollHeight, clientHeight: el.clientHeight, })); for (let i = 0, y = 0; y < scrollHeight; i++, y += clientHeight) { await box.evaluate((el, top) => { el.scrollTop = top; }, y); await page.waitForTimeout(100); await box.screenshot({ path: `chat-${String(i).padStart(2, "0")}.png` }); }

Make screenshots stable between runs

If you compare screenshots or attach them to reports, dynamic content causes noise. Three options handle most of it:

await page.screenshot({ path: "home-stable.png", fullPage: true, animations: "disabled", mask: [page.locator(".last-updated"), page.getByRole("img", { name: "avatar" })], maskColor: "#e2e8f0", style: ` #cookie-banner, .intercom-launcher { display: none !important; } video { visibility: hidden; } `, });

Use mask when the element should stay visible as a placeholder (reviewers see that something was there). Use style with display: none when it should disappear completely. The style option is removed after the capture, so the page is unchanged for the rest of the test.

Fonts are the other common source of differences. Wait for them explicitly before capturing pages with web fonts: await page.evaluate(() => document.fonts.ready).

Size, retina and format

GoalSetting
Sharp retina imagebrowser.newPage({ deviceScaleFactor: 2 }) and keep scale: "device"
Same pixel size on every machinescale: "css"
Mobile screenshotbrowser.newContext({ ...devices["iPhone 15"] })
Small files for thumbnailstype: "webp", quality: 70 or type: "jpeg", quality: 75
Transparent PNG of a logo or widgetomitBackground: true and a transparent page background
Dark modebrowser.newContext({ colorScheme: "dark" })

Get a buffer or base64 instead of a file

// TypeScript: Buffer const buffer = await page.screenshot({ type: "png" }); const base64 = buffer.toString("base64"); const dataUrl = `data:image/png;base64,${base64}`; # Python: bytes import base64 png = page.screenshot() b64 = base64.b64encode(png).decode()

Use the buffer when you upload to S3, send the image to a vision model or attach it to a Slack message. Skipping the disk write also avoids clean-up in CI.

Screenshots in Playwright Test

For tests, let the runner handle screenshots instead of calling page.screenshot() in every test:

// playwright.config.ts import { defineConfig } from "@playwright/test"; export default defineConfig({ use: { screenshot: { mode: "only-on-failure", fullPage: true }, }, });

The modes are off (default), on, only-on-failure and on-first-failure. Screenshots are attached to the test results and show up in the HTML report.

For explicit evidence inside a test, attach the image to the report:

test("checkout summary", async ({ page }, testInfo) => { await page.goto("/checkout"); await testInfo.attach("summary", { body: await page.getByTestId("summary").screenshot(), contentType: "image/png", }); });

Pixel comparisons with toHaveScreenshot() are a separate topic with their own thresholds and baselines. Our visual regression testing guide covers that workflow.

Troubleshooting

SymptomLikely causeFix
Full page is only one screen tallThe app scrolls inside a container, not the documentOption B above
Blank areas where images should beLazy loadingScroll the page before capturing
Text in a fallback fontWeb fonts not loaded yetawait page.evaluate(() => document.fonts.ready)
Image is 2x larger than viewportscale: "device" with deviceScaleFactor: 2scale: "css"
Strict mode violation on element screenshotLocator matches more than one elementUse a narrower locator or .first()
Cookie banner covers the pageConsent manager overlaystyle option with display: none, or click "Reject" first
Screenshot times outElement never becomes visibleCheck the selector, then raise timeout

When you need screenshots outside tests

Playwright is the right tool inside a test suite. For product features such as link previews, scheduled page archives or capturing thousands of customer URLs, you end up running a browser fleet: memory limits, crashed Chromium processes, font packages in Docker and queueing. A screenshot API moves that work off your servers.

The options map almost one to one. Here is the scrollable-page case with SnapshotFlow:

curl --get "https://api.snapshotflow.com/screenshot" \ -H "X-Api-Key: $SNAPSHOTFLOW_API_KEY" \ --data-urlencode "url=https://example.com/dashboard" \ --data-urlencode "full_page=true" \ --data-urlencode "width=1440" \ --data-urlencode "device_scale_factor=2" \ --data-urlencode "format=webp" \ --data-urlencode "styles=html,body,main{height:auto!important;overflow:visible!important}" \ --data-urlencode "hide_selectors=.intercom-launcher" \ --data-urlencode "block_cookie_banners=true" \ -o dashboard.webp

For a full mapping of Playwright options to API parameters and a cost comparison, see Playwright alternative for screenshots.

FAQ

How do I take a full page screenshot in Playwright Python?

page.screenshot(path="page.png", full_page=True). Note the snake_case name; fullPage raises an error in Python.

Can Playwright take a screenshot in WebP?

Yes. type: "webp" is supported in current versions. At the default quality of 100 it is lossless; lower values switch to lossy compression.

Does fullPage work in Firefox and WebKit?

Yes, fullPage works in all three Playwright browsers. Rendering details such as font smoothing differ between engines, so do not compare screenshots across browsers pixel by pixel.

How do I take a screenshot only when a test fails?

Set use: { screenshot: "only-on-failure" } in playwright.config.ts. For Python with pytest-playwright, pass --screenshot only-on-failure.

Sources and further reading

Try it: 300 free screenshots, no card

Keep Playwright for your tests. For screenshots your product or reports depend on, send one HTTP request and get the image back, with full page, retina, WebP and cookie-banner blocking built in.