TL;DR
Use
page.screenshot()for the viewport, addfullPage: truefor the complete document, and calllocator.screenshot()for one element. Stabilize the page before capture by fixing the viewport, waiting for the intended UI state, disabling animations, masking volatile or sensitive regions, and saving the returned buffer when you do not want a local file. Playwright is best when you need browser control; a screenshot API is simpler for large URL queues.
A Playwright screenshot takes one call. A reliable screenshot needs a little more discipline. The shortest JavaScript version is await page.screenshot({ path: "page.png" }). Add fullPage: true to capture the entire scrollable page, or call await page.locator("main").screenshot() to capture one element.
This guide covers those three cases in JavaScript and Python, then tackles the failures that show up in CI and production: unfinished rendering, animation, lazy-loaded content, sticky headers, detached elements, authentication, storage, and concurrency. The examples follow the current Playwright screenshot API and distinguish screenshot capture from visual regression testing. At the end, we compare direct Playwright control with a managed screenshot endpoint for URL-in, image-out jobs.
Take a Playwright Screenshot in JavaScript
Install Playwright and its Chromium browser if the project does not already have them:
npm install playwright
npx playwright install chromiumThen open a page and write the visible viewport to disk:
const { chromium } = require("playwright");
async function capture() {
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: 1440, height: 900 },
});
await page.goto("https://example.com", {
waitUntil: "domcontentloaded",
});
await page.screenshot({ path: "example.png" });
await browser.close();
}
capture().catch(console.error);The path is relative to the process working directory. Playwright infers the format from the extension, so .png creates a PNG and .jpeg creates a JPEG. You can also set type explicitly.
Keep the viewport fixed when screenshots will be compared across runs. A responsive page can produce a completely different layout at 1280 pixels than at 1440 pixels, even if the underlying code has not changed.
Capture the Full Page
The default screenshot only contains the current viewport. Set fullPage: true to capture the complete scrollable document:
await page.screenshot({
path: "example-full-page.png",
fullPage: true,
});Long pages can expose two problems. Lazy-loaded sections may still be empty because they never entered the viewport, and sticky headers can appear repeatedly as Playwright scrolls and stitches the image. If either matters, scroll through the page first and wait for the final piece of content you expect:
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);
});
await page.locator("footer").waitFor();
await page.screenshot({ path: "loaded-page.png", fullPage: true });That fixed 100 ms pause is appropriate for loading content during a capture routine. It should not replace Playwright's condition-based waits in test assertions.
Capture One Element
Use a locator when the output should contain a chart, product card, receipt, or other specific component:
const card = page.locator("[data-testid='product-card']").first();
await card.screenshot({
path: "product-card.png",
animations: "disabled",
});According to the Locator screenshot documentation, Playwright waits for actionability, scrolls the element into view, and then captures it. The call fails if the element becomes detached from the DOM. For a scrollable container, it captures the currently visible part of that container rather than its hidden overflow.
Prefer stable locators such as roles, labels, or test IDs over a chain of generated CSS classes:
await page
.getByRole("dialog", { name: "Order summary" })
.screenshot({ path: "order-summary.png" });Return a Buffer Instead of Writing a File
Omit path when the image should go to object storage, a database, or an HTTP response. The JavaScript API returns a Buffer:
const image = await page.screenshot({
type: "jpeg",
quality: 82,
fullPage: true,
});
await uploadScreenshot(image);quality applies to JPEG and WebP, not PNG. For pixel-accurate regression images, PNG avoids lossy compression. For large archival captures where tiny visual differences do not matter, JPEG or WebP usually creates smaller objects.
Make Screenshots Deterministic
The screenshot call is rarely the unstable part. The page state is. A timestamp changes, a font arrives late, an animation lands on a different frame, or a request finishes after the image was saved.
Start with this stricter capture helper:
async function stableScreenshot(page, url, path) {
await page.goto(url, { waitUntil: "domcontentloaded" });
await page.locator("main").waitFor({ state: "visible" });
await page.evaluate(() => document.fonts.ready);
await page.screenshot({
path,
fullPage: true,
animations: "disabled",
caret: "hide",
mask: [page.locator("[data-volatile]")],
maskColor: "#666666",
style: `
[data-capture-hide] { visibility: hidden !important; }
.sticky-banner { position: static !important; }
`,
});
}Each option fixes a different source of noise:
animations: "disabled"fast-forwards finite animations and cancels infinite ones for the capture.caret: "hide"removes a blinking text cursor.maskplaces solid boxes over selected locators. Use it for rotating ads, timestamps, tokens, email addresses, or other data that should not enter the image.styleinjects a stylesheet for the screenshot. It is useful for neutralizing fixed banners or hiding unstable decorations without changing application code.- A fixed viewport, locale, timezone, and color scheme keep responsive and personalized rendering consistent.
Do not use waitForTimeout(5000) as a universal readiness check. It makes fast pages slow and still loses against a request that takes six seconds. Wait for a product-specific signal instead: a loading indicator becoming hidden, a chart receiving its final label, a response matching an API URL, or a known element becoming visible.
await Promise.all([
page.waitForResponse((response) =>
response.url().includes("/api/dashboard") && response.ok()
),
page.goto("https://app.example.com/dashboard"),
]);
await page.getByTestId("dashboard-ready").waitFor();networkidle is not a guarantee that a modern application is visually complete. Polling, analytics, and streaming connections may keep the network active, while client-side rendering can continue after the network becomes quiet.
Playwright Screenshot in Python
The Python API exposes the same core controls. Here is a synchronous full-page capture:
from playwright.sync_api import sync_playwright
with sync_playwright() as playwright:
browser = playwright.chromium.launch()
page = browser.new_page(viewport={"width": 1440, "height": 900})
page.goto("https://example.com", wait_until="domcontentloaded")
page.screenshot(
path="example-full-page.png",
full_page=True,
animations="disabled",
caret="hide",
)
browser.close()For an element screenshot:
card = page.get_by_test_id("product-card").first
card.screenshot(path="product-card.png", animations="disabled")And for async applications or concurrent workers:
import asyncio
from playwright.async_api import async_playwright
async def capture(url: str) -> bytes:
async with async_playwright() as playwright:
browser = await playwright.chromium.launch()
page = await browser.new_page(
viewport={"width": 1440, "height": 900}
)
await page.goto(url, wait_until="domcontentloaded")
image = await page.screenshot(full_page=True, type="png")
await browser.close()
return image
png_bytes = asyncio.run(capture("https://example.com"))The official Playwright Python screenshot guide confirms that omitting path returns the image bytes. The API also supports clipping, masks, background omission, injected styles, format, quality, and device or CSS pixel scaling.
Screenshot Capture Is Not Visual Regression Testing
A screenshot records pixels. A visual regression test decides whether changed pixels represent a failure.
Playwright Test has its own visual comparison assertion:
import { test, expect } from "@playwright/test";
test("pricing page matches the approved image", async ({ page }) => {
await page.goto("https://example.com/pricing");
await expect(page).toHaveScreenshot("pricing.png", {
animations: "disabled",
maxDiffPixelRatio: 0.01,
});
});The first accepted run creates a baseline. Later runs compare the current rendering against it. Keep baseline generation and comparison environments aligned: operating system, browser version, fonts, device scale, and rendering hardware can all affect pixels.
Use page.screenshot() when the image itself is the deliverable. Use toHaveScreenshot() when the deliverable is a pass or fail signal in a test suite. A managed screenshot API can replace capture infrastructure, but it is not automatically a visual regression runner.
Common Playwright Screenshot Failures
The Screenshot Is Blank or Half Rendered
Wait for the state that proves the page is ready. For an application shell, that may be the disappearance of a skeleton. For a chart, it may be an SVG path count. For a report, it may be a response plus a visible heading. Waiting only for domcontentloaded proves that HTML was parsed, not that the UI finished.
Fonts Change Between Runs
Install the same fonts in local development and CI, then wait for document.fonts.ready. If a remote font can fail, consider a checked-in test font or a fallback that is identical across capture hosts.
The Locator Screenshot Throws
The element may have been replaced during a React render. Resolve the locator close to the screenshot, wait for it to become visible, and capture it after the update settles. Avoid keeping an element handle across navigation or state changes.
Full-Page Images Repeat Headers
Fixed and sticky elements can appear more than once in a stitched capture. Use the screenshot style option to set the offending element to position: static, or capture a specific content container if the header is irrelevant.
Authenticated Pages Redirect to Login
Authenticate once and reuse Playwright storage state in trusted environments. Treat the state file as a secret because it can contain cookies and tokens. Mask personal information before sending screenshots to logs, tickets, or third-party storage.
Large Jobs Exhaust Memory
A browser per URL is simple but expensive. Reuse a browser process, isolate jobs in browser contexts, cap page concurrency, close every page in finally, and stream image buffers to storage rather than retaining a batch in memory. Add job-level timeouts and retries for transient navigation errors.
The Target Blocks Automated Browsers
Screenshots do not bypass access controls. Respect site terms and applicable law, keep request rates reasonable, and do not treat random mouse movement or a changed user agent as a reliable access strategy. If access, proxies, and retry policy become the bulk of the implementation, the build-versus-buy decision has changed.
For broader browser extraction patterns, see our Playwright web scraping glossary and JavaScript scraping guide.
When a Managed Screenshot API Is Simpler
Playwright is the right choice when your workflow needs browser state and precise interaction: log in, click through a wizard, seed application data, capture one component, then assert on the result. You own the browser because the browser flow is part of the product.
A managed endpoint fits a different shape: you have a queue of URLs and need page images without running Chromium pools, patching browser images, rotating failed workers, or managing object storage uploads.
ScrapeGraphAI's scrape endpoint supports a screenshot format alongside Markdown and links. A screenshot costs 2 credits. The supported screenshot controls are fullPage, width, height, and quality; the response includes a hosted image URL plus its dimensions.
curl --request POST \
--url https://v2-api.scrapegraphai.com/api/scrape \
--header "Content-Type: application/json" \
--header "SGAI-APIKEY: $SGAI_API_KEY" \
--data '{
"url": "https://example.com",
"formats": [
{
"type": "screenshot",
"fullPage": true,
"width": 1440,
"height": 900,
"quality": 80
}
]
}'You can request extraction and a screenshot in the same job:
import { ScrapeGraphAI } from "scrapegraph-js";
const sgai = ScrapeGraphAI(); // Reads SGAI_API_KEY from the environment
const bundle = await sgai.scrape({
url: "https://example.com",
formats: [
{ type: "markdown", mode: "reader" },
{ type: "links" },
{
type: "screenshot",
fullPage: false,
width: 1280,
height: 720,
quality: 80,
},
],
});
console.log(bundle);This is page-level capture. Use Playwright directly when you need an element screenshot, interaction before capture, or Playwright Test's image comparison. Use the endpoint when a hosted page image and extracted content are the outputs you need. The ScrapeGraphAI endpoint guide covers the other formats and fetch controls.
| Requirement | Direct Playwright | Managed screenshot endpoint |
|---|---|---|
| Capture one element | Yes | No, page capture only |
| Click or log in before capture | Full control | Not the intended workflow |
| Visual regression assertion | Playwright Test supports it | Build comparison separately |
| Return a hosted image URL | Build the upload path | Included in the response |
| Bundle image with Markdown and links | Build extraction separately | One scrape request |
| Browser fleet and retries | You operate them | Managed |
The practical boundary is simple. If the steps before the screenshot matter, use Playwright. If the screenshot and page content are the deliverables, a managed scrape request removes infrastructure that does not differentiate your product.
Playwright Screenshot Checklist
Before putting a capture job into CI or production, verify these points:
- Fix the viewport, browser version, locale, timezone, and fonts.
- Wait for a business-specific ready condition, not an arbitrary delay.
- Disable animation and hide the caret for stable images.
- Mask secrets, personal data, timestamps, and rotating content.
- Decide between viewport, full-page, and element capture deliberately.
- Return a buffer when storage is remote; do not write temporary files without a reason.
- Close pages and contexts even when navigation or upload fails.
- Cap concurrency based on memory, CPU, and target-site limits.
- Use Playwright Test, not a raw screenshot alone, for visual regression assertions.
- Reconsider self-hosting when URL queues and capture operations outweigh browser interaction.