TL;DR: Playwright screenshots are the fastest way to figure out why a test is actually failing. One page.screenshot() call shows you what the browser saw, which is almost always more useful than whatever cryptic error message you're staring at. Beyond debugging, they're the backbone of visual regression testing, where Playwright compares screenshots against baselines and flags pixel-level differences automatically. This tutorial covers every screenshot type (page, full-page, element, clipped region), how to capture them automatically on failure, and how to set up visual regression so UI bugs stop slipping through unnoticed.
The first time I used Playwright screenshots in a real project, it wasn't for anything sophisticated. A flow was failing intermittently, and the error logs were useless.
They kept saying “element not found" on a button that was right there in the DOM, clearly present, clearly visible in my local browser. I re-ran the test a dozen times, re-checked the selectors, added waits, all to no avail.
I then added a single page.screenshot() call before the failing step, and the screenshot immediately showed a cookie consent banner parked right on top of the checkout button.
The element I was looking for existed and was rendered, but it just had something else sitting on it, and the error message didn’t make it clear. It took about three seconds to add a dismiss step after two hours of guessing at everything else.
Screenshots in test automation are one of those things that feel optional until you need them. Then, suddenly, they’re the only thing that matters.
Playwright's screenshot API is flexible enough to handle everything from quick debugging captures to structured visual regression testing, and it has a quick learning curve as well.
Playwright's screenshot API is flexible enough to handle everything from quick debugging captures to structured visual regression testing, and it has a quick learning curve as well.
How to Take a Basic Page Screenshot in Playwright
The simplest Playwright screenshot captures whatever's currently visible in the viewport. Here's the minimal version:
import { test, expect } from '@playwright/test';
test('capture homepage', async ({ page }) => {
await page.goto('https://example.com');
await page.screenshot({ path: 'homepage.png' });
});That's it. page.screenshot() grabs the visible viewport and saves it to the path you specify. The file format is inferred from the extension, such as .png for PNG and .jpg or .jpeg for JPEG.
PNGs are lossless and larger; JPEGs are smaller but lossy. For debugging and test evidence, PNG is almost always what you want. For bulk screenshots where file size matters, JPEG with a quality setting works:
await page.screenshot({
path: 'homepage.jpg',
quality: 80
});Here’s one thing that tripped me up early: quality only works with JPEG. If you pass it with the .png path, Playwright throws an error. This is probably not a big deal, but it’s one of those pesky errors you end up wasting time on.
If you don't pass a path at all, page.screenshot() returns a Buffer, which is useful when you want to attach the image to a test report or send it somewhere programmatically rather than save it to disk.
const buffer = await page.screenshot();How to Take a Full-Page Screenshot
By default, Playwright only captures the viewport, which means whatever's visible without scrolling. For pages with content below the fold, you need fullPage:
await page.screenshot({
path: 'full-page.png',
fullPage: true
});This stitches together the entire scrollable page into a single image. The resulting PNG can be tall, so be aware of file sizes if you're capturing these frequently in CI.
A gotcha worth knowing is that fullPage doesn't play well with pages that have sticky headers or fixed-position elements. Those elements can render in unexpected positions on full-page captures, especially on WebKit and Firefox.
If your page has a sticky nav and you need a clean full-page screenshot, you might want to suppress the offending elements just for the capture:
await page.locator('header.sticky').evaluate(
el => el.style.visibility = 'hidden'
);
await page.screenshot({ path: 'full-page-clean.png', fullPage: true });How to Capture an Element Screenshot
Sometimes you don't need the whole page but just a specific component. Playwright lets you screenshot any locator:
const card = page.locator('.product-card').first();
await card.screenshot({ path: 'product-card.png' });This captures the element's bounding box, including padding and border, but not the surrounding page content.
I use element screenshots heavily for visual regression on individual components, using them to test that a pricing card, a navigation menu, or a form renders correctly without worrying about the rest of the page.
There is an edge case where if the element is partially off-screen, Playwright scrolls it into view before capturing.
This is usually what you want, but it can trigger lazy-loaded content or scroll-based animations. If that's causing issues, scroll to the element explicitly and add a short wait before capturing.
How to Take a Clipped Screenshot in Playwright
For cases where you want a specific region of the page rather than a specific element, Playwright offers the clip option:
await page.screenshot({
path: 'header-region.png',
clip: {
x: 0,
y: 0,
width: 1280,
height: 200
}
});The clip object defines a rectangle in pixels—x and y for the top-left corner, width, and height for the dimensions.
This is handy when you want to capture a visual region that doesn't correspond neatly to a single DOM element, like a section of the page that spans multiple components or a specific area of a canvas element.
I'll be honest. I don't reach for the clip often. Element screenshots cover most use cases more cleanly because they adapt to the element's actual size.
But for canvas-based apps, map views, or generated charts where the "element" is a single <canvas> and you want a portion of it, clip is the right tool.
How to Capture Screenshots on Test Failure
This is where screenshots go from "nice to have" to "how did we ever debug without these?" Playwright has built-in support for automatic screenshots on failure through the test configuration:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
screenshot: 'only-on-failure'
}
});With this setting, every test that fails automatically gets a screenshot attached to the test report.
That means no extra code in your tests, no try/catch blocks, and no manual page.screenshot() calls. The screenshot captures the page state at the moment of failure, which is almost always more informative than the error message alone.
For CI pipelines, ‘only-on-failure' is the sweet spot, since you get the evidence when you need it without bloating your artifact storage with screenshots of passing tests.
You can also capture screenshots manually in an afterEach hook if you want more control:
test.afterEach(async ({ page }, testInfo) => {
if (testInfo.status !== testInfo.expectedStatus) {
const screenshot = await page.screenshot();
await testInfo.attach('failure-screenshot', {
body: screenshot,
contentType: 'image/png'
});
}
});This gives you the same automatic capture but lets you add custom naming, capture multiple screenshots, or add additional context alongside the image.
Taking screenshots is easy. The harder question is what to do with them! Visual regression testing is the most powerful answer to this.
How Screenshots Support Visual Regression Testing
Taking screenshots is easy. The harder question is what to do with them! Visual regression testing is the most powerful answer to this.
The idea is simple: capture a screenshot of a page or component, save it as a baseline, and on subsequent runs, compare the new screenshot against the baseline. If the pixels differ beyond a threshold, the test fails. Playwright has this built in with toHaveScreenshot():
test('homepage visual check', async ({ page }) => {
await page.goto('https://example.com');
await expect(page).toHaveScreenshot('homepage.png');
});The first time you run this, Playwright creates a baseline screenshot. On subsequent runs, it captures a new screenshot and compares it.
If the images differ, the test fails, and Playwright generates a diff image showing exactly what changed. You can control the sensitivity with maxDiffPixelRatio or maxDiffPixels:
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixelRatio: 0.01
});This tolerates up to 1% pixel difference, which helps with rendering inconsistencies across environments.
Element-level visual regression works the same way:
const nav = page.locator('nav.main');
await expect(nav).toHaveScreenshot('main-nav.png');This is often more practical than full-page visual regression because components change independently. A footer update shouldn't fail your header visual test.
In order to update baselines after intentional changes, run with the --update-snapshots flag:
npx playwright test --update-snapshotsBest Practices for Playwright Screenshots
We’ve explored how different functions work to aid our effort of taking screenshots. Let’s also take a quick look at some best practices for doing so:
1. Name Your Screenshots Descriptively
screenshot-1.png tells you nothing when you’re stuck in an issue six months post its first usage, but perhaps checkout-payment-form-error-state.png tells you everything. This matters more than you'd think when you're digging through CI artifacts late at night, tired and lost.
2. Set a Consistent Viewport Size
Screenshots vary with viewport dimensions, which creates noise in visual regression. You can lock it down in your config:
use: {
viewport: { width: 1280, height: 720 }
}3. Wait for the Page to Stabilize Before Capturing
Animations, lazy loading, and font rendering can all produce inconsistent screenshots. The Playwright team explicitly discourages page.waitForLoadState('networkidle') for tests since it's flaky on pages with long-poll connections, analytics beacons, or websockets.
Use web-first assertions against an element you actually care about:
await expect(page.getByRole('heading', { name: 'Checkout' })).toBeVisible();
await page.screenshot({ path: 'checkout.png' });4. Store Baseline Screenshots in Version Control
They should be a mandatory part of your test suite. Hence, when a visual change is intentional, the baseline update should be in the same PR as the code change so reviewers can see both.
What's Next: AI Testing Without the Framework Overhead
If you've followed this tutorial and you're thinking, "Great, now I need to build a test framework, a CI pipeline, a reporting dashboard, and maintain all of it," that’s a valid concern! Playwright's API is excellent, but the infrastructure around it is where teams burn time.
That's where AI can change the equation. Autify Aximo is one such tool that skips the framework layer entirely. Instead of writing and maintaining scripts, you author tests in natural language, and Aximo executes them using visual recognition. No selectors to update, no test framework to babysit, no CI plumbing to build from scratch.
Because vendor lock-in and existing architecture is a real concern, Aximo offers a premium Playwright script generator so you can walk away with your tests as runnable Playwright code whenever you want.

FAQ
Can Playwright Take a Screenshot?
Yes. Playwright supports page screenshots, full-page screenshots, element screenshots, and clipped region screenshots. The API is a single method call—page.screenshot() for pages, locator.screenshot() for elements—with options for format, quality, and region.
How Do I Add a Screenshot to a Playwright Report?
Set screenshot: 'only-on-failure' (or 'on') in your playwright.config.ts under the use section. Failed tests will automatically include screenshots in the HTML report. For manual control, use testInfo.attach() in an afterEach hook.
How Do Screenshots Help with Visual Regression Testing?
Playwright's toHaveScreenshot() method captures a screenshot, saves it as a baseline, and on subsequent runs compares new captures pixel-by-pixel.
If differences exceed your configured threshold, the test fails and generates a diff image showing exactly what changed. This catches unintended visual regressions that functional tests would miss entirely.
