{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreia5inube27kcuxucdxtv7bqkq3xoa2tzlp2vmaflq6lsdinoy33nm",
"uri": "at://did:plc:25rdn5elo5izoxrmtis34zuk/app.bsky.feed.post/3moztupsfujl2"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreifz6fiiftxvi2woatbwtslibee7fykmscpagcuoyen4scwjtzqlvm"
},
"mimeType": "image/webp",
"size": 79496
},
"path": "/grabbit/how-to-take-screenshots-in-playwright-full-page-elements-ci-11gc",
"publishedAt": "2026-06-24T11:37:41.000Z",
"site": "https://dev.to",
"tags": [
"webdev",
"javascript",
"node",
"programming",
"visual regression testing guide",
"screenshot API",
"Puppeteer screenshot guide",
"screenshot API comparison",
"Grabbit blog"
],
"textContent": "`page.screenshot()` does the job in two lines for a local capture or a test assertion. The friction arrives later: full-page shots that get clipped by sticky headers, Chromium installation issues in CI, and the long tail of keeping a browser fleet running in production. This guide covers the screenshot methods that work, the ones with hidden edge cases, and the point where offloading to an API makes more sense.\n\n## The basic Playwright screenshot\n\nLaunch a browser, open a page, capture:\n\n\n\n import { chromium } from 'playwright';\n\n const browser = await chromium.launch();\n const page = await browser.newPage();\n\n await page.goto('https://example.com', { waitUntil: 'networkidle' });\n await page.screenshot({ path: 'example.png' });\n\n await browser.close();\n\n\nThe `waitUntil: 'networkidle'` option is worth stating explicitly. The default `load` event fires as soon as the HTML and blocking scripts are done, but client-rendered content, web fonts, and lazy-loaded images often arrive later. `networkidle` waits until there are no network connections open for at least 500ms, which is a much better proxy for \"the page actually looks right.\"\n\n## Full-page screenshots\n\nBy default `page.screenshot()` captures the viewport only. To capture the full scrolling page, set `fullPage: true`:\n\n\n\n await page.screenshot({ path: 'full.png', fullPage: true });\n\n\nPlaywright expands the viewport to the document's full scroll height before capturing. This works reliably on most static pages.\n\nTwo things break it:\n\n * **Sticky and fixed-position elements.** A header with `position: fixed` renders in its normal viewport position for every \"slice\" Playwright captures, so it can appear repeated or floating over content in the final stitched image. There is no clean built-in workaround short of hiding the element with a `page.addStyleTag` before capturing.\n * **Lazy-loaded content.** If a page only renders sections when they scroll into view, `fullPage: true` can come back short. Scroll the page yourself before capturing to trigger those renders:\n\n\n\n\n await page.evaluate(async () => {\n await new Promise<void>((resolve) => {\n let scrolled = 0;\n const step = 500;\n const timer = setInterval(() => {\n window.scrollBy(0, step);\n scrolled += step;\n if (scrolled >= document.body.scrollHeight) {\n clearInterval(timer);\n window.scrollTo(0, 0);\n resolve();\n }\n }, 100);\n });\n });\n\n await page.screenshot({ path: 'full.png', fullPage: true });\n\n\n## Screenshotting a specific element\n\nPlaywright's locator API makes element-level captures cleaner than Puppeteer's `ElementHandle` approach. Call `.screenshot()` on any locator and Playwright crops to that element's bounding box:\n\n\n\n // Capture a single component by CSS selector\n await page.locator('#pricing-card').screenshot({ path: 'card.png' });\n\n // Or by role\n await page.getByRole('dialog').screenshot({ path: 'modal.png' });\n\n\nIf the element is below the fold, Playwright scrolls it into view before capturing. If the selector matches nothing, the call throws, so wrap it in a `waitFor` when the element might not be immediately present:\n\n\n\n const card = page.locator('#pricing-card');\n await card.waitFor({ state: 'visible' });\n await card.screenshot({ path: 'card.png' });\n\n\n## Format and quality options\n\nPlaywright defaults to PNG. For production use cases where file size matters, switch to JPEG or WebP:\n\n\n\n await page.screenshot({\n path: 'page.webp',\n type: 'webp',\n quality: 85, // 0–100, only for jpeg and webp\n });\n\n\nPNG is lossless and best for visual regression tests where pixel-level accuracy matters. JPEG and WebP are better for images that will be served over HTTP, since they are significantly smaller at comparable quality.\n\n## Screenshots in CI with GitHub Actions\n\nInstalling Playwright in CI requires browser binaries and their system dependencies. Use `npx playwright install --with-deps` (not just `npx playwright install`) to pull in the OS libraries Chromium needs:\n\n\n\n - name: Install Playwright browsers\n run: npx playwright install --with-deps chromium\n\n - name: Run tests\n run: npx playwright test\n\n - name: Upload test artifacts\n if: always()\n uses: actions/upload-artifact@v4\n with:\n name: playwright-report\n path: test-results/\n retention-days: 7\n\n\nThe `if: always()` on the upload step is important: test artifacts are most useful when tests fail, and a plain `if: success()` would skip the upload precisely when you need the diffs.\n\nPlaywright's `--with-deps` flag is the most common CI pitfall. Without it, Chromium launches fine locally (because those libraries are already installed) but fails in a minimal CI container with a cryptic `error while loading shared libraries` message.\n\n## Visual regression with screenshots\n\nPlaywright's built-in snapshot assertion compares a screenshot against a stored baseline:\n\n\n\n await expect(page).toHaveScreenshot('homepage.png');\n\n\nOn the first run, Playwright writes the baseline to `__snapshots__/`. On subsequent runs it compares pixel-by-pixel and fails if the diff exceeds the configured threshold. Pass `{ maxDiffPixelRatio: 0.01 }` to tolerate minor anti-aliasing differences:\n\n\n\n await expect(page.locator('.hero-section')).toHaveScreenshot('hero.png', {\n maxDiffPixelRatio: 0.01,\n });\n\n\nFor a longer look at visual regression workflows, the visual regression testing guide covers when snapshot tests make sense and how to keep them from becoming brittle.\n\n## Where running Playwright yourself gets costly\n\nPlaywright is the right tool for test suites that already drive a browser. When screenshots are a standalone production feature, the overhead adds up:\n\n * **Chromium in production.** Deploying a headless browser to a serverless function means bundling system libraries, increasing cold-start times, and hitting the payload size limits of most platforms.\n * **Concurrency.** One browser instance handles one capture at a time well. Handling bursts means a pool, a queue, and back-pressure logic.\n * **Security surface.** A browser that can render arbitrary user-submitted URLs needs SSRF protection (block private IP ranges), sandboxing, and regular security patches.\n\n\n\nThis is the point where a dedicated screenshot API is faster to operate: the browser fleet, SSRF guards, and scaling are someone else's problem.\n\n## The same capture as an API call\n\nHere is the full-page capture above as a single request to Grabbit. No browser to provision or patch:\n\n\n\n curl https://api.grabbit.live/v1/grabs \\\n -H \"Authorization: Bearer sk_live_...\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\n \"url\": \"https://example.com\",\n \"width\": 1280,\n \"height\": 720,\n \"full_page\": true,\n \"format\": \"webp\"\n }'\n\n\nThe response includes a hosted `image_url` you can use directly:\n\n\n\n {\n \"id\": \"grb_01jx...\",\n \"status\": \"done\",\n \"image_url\": \"https://cdn.grabbit.live/grabs/grb_01jx....webp\",\n \"width\": 1280,\n \"format\": \"webp\",\n \"bytes\": 52340,\n \"execution_ms\": 1240\n }\n\n\nThe Playwright options you reach for most translate directly: `fullPage: true` becomes `\"full_page\": true`, the element locator pattern becomes a `\"selector\"` field, and manual wait time becomes `\"delay_ms\"` (0 to 10000). Width accepts 320 to 1920, height 240 to 1080, and `\"format\"` is `\"png\"`, `\"jpeg\"`, or `\"webp\"`.\n\n\n\n # capture one component after waiting for it to settle\n -d '{\n \"url\": \"https://example.com/dashboard\",\n \"selector\": \"#chart-container\",\n \"delay_ms\": 800,\n \"format\": \"png\",\n \"width\": 1280,\n \"height\": 720\n }'\n\n\n## Which to use\n\nUse Playwright directly when screenshots are part of a test suite that already controls a browser, or when you need precise assertions against local builds that are not reachable from the internet. Use an API when screenshots are a production feature and you would rather ship in an afternoon than build a browser fleet.\n\nFor a head-to-head look at Puppeteer's API versus Playwright's for captures, see the Puppeteer screenshot guide. If you are picking a hosted screenshot service, the screenshot API comparison covers the trade-offs honestly.\n\n_Originally published on the Grabbit blog._",
"title": "How to Take Screenshots in Playwright (Full Page, Elements, CI)"
}