How to use Playwright for screenshot testing
Build Playwright screenshot tests with visual baselines, browser projects, stable waits, diff thresholds, and update review.

Playwright screenshot testing is for UI changes you want to catch before they ship. The test opens a page, waits for a stable state, captures an image, and compares it with an approved baseline. That is the difference between ordinary screenshot automation and visual regression testing.
The examples here use Shotomatic's features page, https://www.shotomatic.com/features, as a public target. The same pattern is more useful in your own app, where you can control data, login state, animations, and test selectors.
If you only need image files from a list of URLs, start with plain Playwright screenshot automation. This article is for the testing version: baselines, screenshot comparison, browser projects, and review.
When screenshot testing is worth it
Screenshot tests are useful when a visual mistake would be easy to miss in ordinary assertions.
Good candidates:
- marketing pages with important hero, pricing, or signup sections
- design system components with many states
- checkout, onboarding, or dashboard screens where layout matters
- responsive pages that often break at mobile widths
- pages where CSS changes can silently move or hide content
Poor candidates:
- pages with constantly changing feeds, ads, timestamps, or user content
- pages where visual polish does not matter
- flows that are already covered by stronger text, role, or behavior assertions
- whole sites with hundreds of unstable pages and no review process
Start with a few important screens. Screenshot testing gets noisy when you ask it to watch everything.
Install Playwright Test
For a new or existing Node project, the official Playwright installer can scaffold the test runner, config file, example tests, and browser install step:
$ npm init playwright@latest
The installer asks whether to use TypeScript or JavaScript, where to put tests, whether to add a GitHub Actions workflow, and whether to install browsers. After setup, run the tests:
$ npx playwright test
For this article, the examples use TypeScript files under tests/.
Create the first screenshot test
Create tests/features-page.spec.ts:
import { expect, test } from "@playwright/test";
test("features page has a stable desktop layout", async ({ page }) => {
await page.goto("https://www.shotomatic.com/features");
await expect(
page.getByRole("heading", {
name: /Shotomatic Features/i,
}),
).toBeVisible();
await expect(page).toHaveScreenshot("features-page-desktop.png", {
fullPage: true,
});
});
The important difference from page.screenshot() is the assertion. Playwright Test's toHaveScreenshot() creates or compares against a baseline. The first run writes the missing baseline. Later runs compare the current screenshot with that saved image.
Run the test:
$ npx playwright test tests/features-page.spec.ts
On the first run, Playwright writes a new expected image because no baseline exists yet. Review it before committing it. A baseline is a test expectation, not a random artifact.
Test a smaller part of the page
Full-page screenshots catch broad layout shifts, but they are also more likely to fail because of unrelated changes far down the page. For many teams, a section or component baseline is easier to maintain.
import { expect, test } from "@playwright/test";
test("features comparison cards stay aligned", async ({ page }) => {
await page.goto("https://www.shotomatic.com/features");
const comparisonHeading = page.getByRole("heading", {
name: /Choose a workflow/i,
});
const comparisonSection = page.locator("section").filter({
has: comparisonHeading,
});
await expect(comparisonHeading).toBeVisible();
await expect(comparisonSection).toHaveScreenshot("features-comparison-section.png");
});
Use locator screenshots when the review question is local: did this section still look right? Use page screenshots when the whole page layout matters.
Add browser and device projects
Playwright's test runner can run the same test across named projects. That is where screenshot testing starts to feel different from a simple capture script: the browser, device, viewport, and snapshot name all become part of the test matrix.
Example playwright.config.ts:
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests",
use: {
baseURL: "https://www.shotomatic.com",
trace: "on-first-retry",
},
projects: [
{
name: "desktop-chromium",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1440, height: 1000 },
},
},
{
name: "mobile-safari",
use: {
...devices["iPhone 13"],
},
},
],
});
Then use relative URLs in the test:
import { expect, test } from "@playwright/test";
test("features page visual baseline", async ({ page }) => {
await page.goto("/features");
await expect(page.getByRole("main")).toBeVisible();
await expect(page).toHaveScreenshot("features-page.png", {
fullPage: true,
});
});
Playwright stores different snapshots for different projects, so desktop and mobile baselines can live beside each other without pretending they should match. This is a better fit than one viewport screenshot when the question is whether the UI works across device sizes.
Make the page state stable
Many noisy screenshot failures come from unstable page state. Playwright disables CSS animations and transitions during screenshot assertions by default, but changing data, delayed content, ads, and user-specific elements can still move pixels.
Before taking a screenshot, wait for something specific:
await page.goto("/features");
await expect(page.getByRole("main")).toBeVisible();
await expect(page.getByText("Hands-Free Capture")).toBeVisible();
Avoid relying on a fixed timeout as your main wait rule. If the page needs a card, heading, table, or loaded image before it is ready, wait for that condition directly.
If the page lazy-loads below-the-fold content, scroll before a full-page screenshot:
await page.evaluate(async () => {
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const viewportHeight = window.innerHeight;
for (let y = 0; y < document.body.scrollHeight; y += viewportHeight) {
window.scrollTo(0, y);
await delay(100);
}
window.scrollTo(0, 0);
});
That kind of helper is fine when the page needs it. Keep it close to the test so future maintainers know why the scroll exists.
Hide or mask volatile elements
Some parts of a page should not participate in visual comparison: timestamps, avatars, ads, user-specific names, loading spinners, or video thumbnails.
You have two common options.
Mask specific locators:
await expect(page).toHaveScreenshot("features-page.png", {
fullPage: true,
mask: [page.locator("[data-testid='release-date']")],
});
Or apply a stylesheet while the screenshot is taken:
await expect(page).toHaveScreenshot("features-page.png", {
fullPage: true,
stylePath: "./tests/screenshot.css",
});
Example tests/screenshot.css:
[data-testid="release-date"],
[data-testid="animated-cursor"] {
visibility: hidden !important;
}
Do not hide real product UI just to make tests pass. Masking is for noisy content that is not part of the visual question.
Set a diff threshold carefully
Playwright can allow a limited amount of pixel difference:
await expect(page).toHaveScreenshot("features-page.png", {
fullPage: true,
maxDiffPixels: 100,
});
This is useful for small rendering differences, but it can also hide real regressions. Keep thresholds low, document why they exist, and prefer making the page more deterministic before loosening the comparison.
You can also share screenshot assertion defaults in playwright.config.ts:
import { defineConfig } from "@playwright/test";
export default defineConfig({
expect: {
toHaveScreenshot: {
maxDiffPixels: 100,
},
},
});
Review and update baselines
When a screenshot test fails, the question is not "how do we make it green?" The question is "is this visual change expected?"
If the change is a bug, fix the UI and run the test again. If the change is intentional, update the baseline:
$ npx playwright test --update-snapshots
Then review the new baseline image in the pull request. Treat snapshot updates like code changes. Someone should understand what changed and why.
Run screenshot tests in CI
Screenshot comparisons are sensitive to environment. Fonts, operating system, browser version, hardware, and headless mode can all affect pixels. For stable results, generate and compare baselines in the same environment.
In practice, that usually means:
- commit baseline snapshots generated in CI or in a matching local container
- run the screenshot project on pull requests
- avoid mixing baselines from macOS with Linux CI unless you expect differences
- keep Playwright and its browser binaries updated intentionally
- review changed snapshots before accepting them
Use Playwright's HTML report or trace viewer when a failure is hard to understand. A failed screenshot test needs more review context than a red build can give you.
Where plain screenshot automation still fits
Screenshot testing is for code-owned UI quality checks. It is not the same job as collecting screenshots for review, reporting, archive, or content workflows.
Use plain Playwright screenshot automation when you need output files from URLs.
Use Puppeteer screenshot automation when you want a small Chrome-focused script and do not need Playwright Test.
Use Website Capture in Shotomatic when the task is no-code URL-list capture: add URLs, adjust capture options, review the results, and export screenshots without maintaining a test suite. For teams doing responsive screenshot review by hand, that can be enough. For automated pass/fail testing, keep the workflow in Playwright Test.
Common mistakes
- Testing too much of the page: start with important sections before full-page baselines.
- Capturing unstable data: use fixed test data, masks, or styles for content that changes every run.
- Updating snapshots blindly: only update baselines after reviewing the visual difference.
- Running baselines on one machine and comparing on another: keep the rendering environment consistent.
- Treating visual tests as behavior tests: pair screenshots with normal assertions for text, roles, navigation, and interactions.
FAQ
Can Playwright do screenshot testing?
Yes. Playwright Test includes screenshot assertions with expect(page).toHaveScreenshot() and expect(locator).toHaveScreenshot().
How is screenshot testing different from saving screenshots?
Saving screenshots creates image files. Screenshot testing compares new screenshots against approved baselines and fails the test when the difference is outside the allowed threshold.
Where does Playwright store screenshot baselines?
By default, Playwright stores baselines in a directory named after the test file with a -snapshots suffix. You can customize the path with snapshotPathTemplate in the Playwright config.
How do I update Playwright screenshot baselines?
Run Playwright Test with --update-snapshots after reviewing that the visual change is intentional.
Should I use Playwright screenshot tests for every page?
No. Use screenshot tests for important, stable UI states. Use ordinary assertions for behavior and reserve visual baselines for pages or components where layout changes matter.
References
The examples above were checked against the current official docs:
- Playwright's installation guide for Playwright Test setup
- Playwright's visual comparisons guide for
toHaveScreenshot(), baselines, update flow, and comparison options - Playwright's
PageAssertions.toHaveScreenshot()API forfullPage,mask,stylePath,maxDiffPixels,threshold, and related options - Playwright's assertions guide for auto-retrying web assertions
- Playwright's running and debugging tests guide and trace viewer guide for reviewing failed tests
Related posts
See more postsHow to Automate Website Screenshots with Playwright
Use Playwright to automate website screenshots with JavaScript. Capture one page, full pages, mobile views, and URL batches with retries.

How to Automate Website Screenshots with Puppeteer
Use Puppeteer to automate website screenshots with JavaScript. Capture one page, full-page screenshots, URL batches, and mobile viewports with a reusable script.

How to Automate Website Screenshots Without Code
Choose a no-code way to automate website screenshots: URL-list capture, an automation platform with a screenshot API, monitoring tools, or browser helpers.

Best Tango Alternatives for Mac in 2026
Compare Tango alternatives for Mac across local guide creation, hosted workspaces, desktop capture, in-app guidance, video, and export.

Need no-code website screenshot automation?
Use Website Capture for URL-list screenshots, per-page capture options, reviewable results, and exports without maintaining a script.