Playwright Visual Testing: A Complete Guide

Playwright visual testing guide covering setup, baselines, CI stability, diff tuning, and fixing visual failures with screenshots and snapshots.

User

Pratik Patel

Jan 2, 2026

Playwright Visual Testing: A Complete Guide

Imagine you ship a UI change that passes every functional test.

Then a user opens the page, and something looks off: the button shifts, the layout breaks, or a banner covers the screen.

That is what Playwright visual testing is for.

Playwright visual testing compares screenshots against baseline images and fails tests when pixels differ beyond configured tolerances.

In this Playwright visual testing guide, you will set up a minimal visual test, stabilize it in CI, tune the diff settings, and review failures with clear expected, actual, and diff images.

Visual regression testing is essential for UI development to ensure interface consistency and catch visual changes that might otherwise go unnoticed.

What Playwright visual testing does

Playwright visual testing, also known as Playwright visual regression testing, is a type of snapshot testing for your UI.

When you run a test with toHaveScreenshot():

1. The test runner executes the test file and manages the test run, capturing a screenshot (page or element) and storing it as a test snapshot.

2. The first run creates a baseline file.

3. Later runs (subsequent runs) compare the current UI state against the pre-approved baseline image (test snapshot).

4. A mismatch fails the test and produces diff artifacts. This baseline and diff loop is the core of Playwright visual testing.

Now that you know what happens, here is the smallest possible test you can run.

Quick start with toHaveScreenshot()

1. Install Playwright

If you already have Playwright installed, skip this.

bash
npm init -y npm install -D @playwright/test npx playwright install

2. Write your first visual test

Begin with a full-page snapshot to view the complete Playwright visual testing workflow.

Create a new test file where you will write your home page visual test. This test will capture a screenshot of the homepage and compare it to the baseline to ensure visual integrity for Playwright visual testing.

example.spec.ts
import { test, expect } from '@playwright/test'; test('homepage looks correct', async ({ page }) => { await page.goto('https://testdino.com/'); await expect(page).toHaveScreenshot(); });

3. Run once to create a baseline

On the first run, Playwright needs a baseline image to compare against. If no snapshot exists yet, Playwright will save the “actual” screenshot and show an error. This first failure is normal in Playwright visual testing.

Run your tests:

bash
npx playwright test

Output:

Now run the same command one more time. Playwright will compare the current screenshot against the saved baseline:

bash
npx playwright test

Output:

Best Practice:

Use --update-snapshots only when you intentionally changed the UI and want to update the baseline.

Pixel Perfect

Catch UI regressions your eyes might miss.

Start Visual Check

Stability checklist for reliable visual tests

Visual diffs are usually caused by timing, rendering, or environment drift. This checklist fixes the most common issues.

Running visual tests in a consistent environment, such as a dedicated test environment or CI, is crucial to avoid false positives caused by device or setup differences. This ensures that screenshots are created and compared under the same conditions, making your Playwright visual testing more reliable.

To ensure reliable Playwright visual testing, follow these steps:

  • Wait for the page to settle
  • Disable animations and transitions
  • Wait for fonts
  • Lock your viewport per project
  • Mask dynamic regions

Wait for the page to settle

For Playwright visual testing, take screenshots after your UI is actually ready:

docs-overview.spec.ts
import { test, expect } from '@playwright/test'; test('hero section looks correct', async ({ page }) => { await page.goto('https://docs.testdino.com/overview/'); await page.waitForLoadState('networkidle'); await page.locator('.hero'); await expect(page).toHaveScreenshot('docs-overview.png'); });

Output:

Disable animations and transitions

Animations are one of the fastest ways to create flaky diffs in Playwright visual testing

pricing.spec.ts
import { test, expect } from '@playwright/test'; test('product page looks correct', async ({ page }) => { await page.goto('https://testdino.com/pricing/'); await page.addStyleTag({ content: ` *, *::before, *::after { animation-duration: 0s !important; transition-duration: 0s !important; } ` }); await expect(page).toHaveScreenshot('pricing-page.png'); });

Output:

Wait for fonts

In Playwright visual testing, font loading differences appear as layout shifts and changes in text rasterization.

fonts.spec.ts
import { test, expect } from '@playwright/test'; test('homepage looks correct', async ({ page }) => { await page.goto('https://www.youtube.com/'); await page.evaluate(() => document.fonts.ready); await expect(page).toHaveScreenshot('font.png', { fullPage: true }); });

Output:

Lock your viewport per project

If the viewport changes, the layout changes. Define it once in your config. This is especially important for Playwright visual testing in CI.

For advanced browser testing and reporting, consider using the TestDino Playwright Dashboard.

sample.spec.ts
import { defineConfig } from '@playwright/test'; export default defineConfig({ projects: [ { name: 'desktop', use: { viewport: { width: 1280, height: 720 } } }, { name: 'mobile', use: { viewport: { width: 375, height: 667 } } } ] });

This config sets the browser window size for each Playwright project, ensuring your UI renders consistently on every run.

width and height are the viewport dimensions in pixels, which control responsive breakpoints and layout.

1280 × 720 is a common desktop baseline, and 375 × 667 matches a typical mobile screen size, allowing you to catch desktop and mobile layout changes consistently.

Mask dynamic regions

Mask anything that is expected to change (timestamps, ads, avatars). mask tells Playwright to ignore specific parts of the screenshot during comparison. Masking keeps Playwright visual testing focused on real UI changes, not dynamic content noise.

example.spec.ts
import { defineConfig } from '@playwright/test'; await expect(page).toHaveScreenshot({ mask: [ page.locator('.timestamp'), page.locator('.user-avatar'), page.locator('.ad-banner') ] });

These page.locator(...) entries point to parts of the UI that change between runs, even when nothing is actually broken. By masking them, Playwright ignores pixel diffs in those regions, so your visual test only fails for meaningful UI changes.

Each page.locator(...) points to a dynamic element, like a timestamp, avatar, or ad, that can change between runs.

  • Timestamps change every run.
  • Avatars can load late or vary by user or environment.
  • Ads and banners are often personalized, A/B tested, or injected by third parties.

Those areas are still visible in the screenshot, but differences inside them will not fail the test.

Diff settings that reduce false alarms

Playwright provides configuration options for toHaveScreenshot() for visual testing, that you can adjust to reduce noisy diffs without hiding real UI regressions.

What the common options mean:

Option What it controls Good starting point
maxDiffPixels Absolute number of pixels allowed to differ 50 to 200
maxDiffPixelRatio Percent of pixels allowed to differ 0.005 to 0.02
threshold Per-pixel color difference tolerance 0.1 to 0.3

Here is a practical default you can apply to noisy pages:

homepage-thresholds.spec.ts
import { test, expect } from '@playwright/test'; test('homepage looks correct', async ({ page }) => { await page.goto('https://testdino.com/', { timeout: 60000 }); await expect(page).toHaveScreenshot('homepage.png', { fullPage: true, maxDiffPixels: 200, maxDiffPixelRatio: 0.01, threshold: 0.2 }); });

Output:

If your failures are mostly related to text rendering, start by increasing the tolerance slightly.

If your failures are missing elements or layout shifts, focus on strict tolerance and prioritize fixing stability instead.

Next, you need a baseline strategy that does not fall apart in CI.

Spot the Difference

Did the button move 2 pixels? Catch UI regressions your eyes might miss.

Start Visual Testing

Manage baselines safely

Keep baselines in version control

In Playwright visual regression testing, baselines are part of your test suite. Commit them so every developer and every CI run compares against the same reference.

Update snapshots deliberately

Do not run --update-snapshots as a reflex. Review the diff images in Git before committing.

Use per-branch snapshot folders when needed

If you maintain long-lived branches with different UI, isolate baselines per branch.

playwright.config.ts
import { defineConfig } from '@playwright/test'; const branch = process.env.BRANCH_NAME || 'main'; export default defineConfig({ snapshotPathTemplate: `{testDir}/visual-baselines/${branch}/{testFilePath}/{arg}{ext}` });

Now you can move from local runs to CI with fewer surprises.

Run visual tests in CI

Here is a minimal GitHub Actions pattern:

playwright.yml
- name: Run Playwright tests run: npx playwright test

To compare test reporting tools, see Compare TestDino to Other Top Test Reporting Tools.

If you send results to a dashboard, uploads must run even when tests fail. Most of the time, you only care about artifacts when the run is red.

playwright.yml
- name: Upload to TestDino if: always() run: npx tdpw upload ./playwright-report --token="${{ secrets.TESTDINO_TOKEN }}" --upload-full-json

Playwright already generates everything you need: expected, actual, and diff images. The problem is where those artifacts live once the test runs in CI.

Here is the flow most teams end up wanting.

Upload and review visual diffs in TestDino

Playwright produces screenshots and diff images locally. TestDino makes them reviewable in one place, tied to your branch and commit context.

Here is the flow most teams end up wanting:

1. A visual test fails in CI

A screenshot assertion fails, and Playwright writes the expected, actual, and diff images into the run artifacts.

At this point, the question is not “did it fail?” The question is:

Is this a real UI regression or just rendering noise?

2. Artifacts are uploaded automatically

To make the failure reviewable, you upload the report output from CI.

Before the upload works reliably, your Playwright config needs to produce artifacts in a predictable folder.

playwright.config.ts
import { defineConfig } from '@playwright/test'; export default defineConfig({ reporter: [ ['json', { outputFile: './playwright-report/report.json' }], ['html', { outputDir: './playwright-report' }] ], use: { screenshot: 'only-on-failure', trace: 'on-first-retry' } });

Now, CI has the same output shape every run, so the uploader can be simple.

bash
npx tdpw upload ./playwright-report --token="your-api-key" --upload-full-json

If you only want image artifacts, upload images explicitly:

bash
npx tdpw upload ./playwright-report --token="your-api-key" --upload-images

3. A reviewer opens the run

Instead of hunting through CI artifacts, the reviewer opens the run and clicks into the failed test.

This is where the review becomes fast. The context is already attached to the run: branch, commit, attempt, and browser project.

4. Diff first, then confirm with expected and actual

Start with the diff view. It answers “what moved” in seconds by allowing you to visually compare the expected, actual, and diff images to identify any changes quickly. This is the fastest way to review Playwright visual comparisons.

Then flip to expected and actual to confirm whether it is a real regression or just noise.

In TestDino, the Visual Comparison panel supports:

  • Diff
  • Actual
  • Expected
  • Side by side
  • Slider

5. Use the same quick triage every time

This is the review loop that keeps visual testing from turning into a time sink:

1. Check which browser projects failed (chromium, firefox, webkit)

2. Check attempts and retries to spot flakes versus consistent failures

3. Open diff first, then validate in expected and actual

4. If many tests fail together, suspect a shared CSS or layout change

That is it. You are not trying to “analyze screenshots.” You are trying to answer one question quickly: regression or noise.

Now, let’s cover the failures you will see in real projects when doing visual regression testing with Playwright.

Troubleshooting common visual failures

Most Playwright visual regression testing failures fall into a few repeatable patterns.

Screenshot does not match baseline

This is the normal failure mode.

  • If the UI change is intentional, update snapshots and commit
  • If the UI change is not intentional, fix the code
  • If it is noise, stabilize timing, fonts, animations, or masking

Baseline not found

This happens on new tests, new browsers, or when snapshots are deleted.

  • Run the test locally once to generate the baseline
  • Commit the snapshot file
  • Ensure CI uses the same snapshot files

Passes locally but fails in CI

This is almost always environment drift.

  • Different OS font rasterization
  • Different browser versions
  • Missing fonts in CI
  • Different viewport or locale

Fix it by locking the viewport, waiting for fonts, disabling animations, and running in Docker when needed.

Conclusion

Playwright visual testing is straightforward to start: add toHaveScreenshot(), run once to create baselines, then compare on every change.

Visual testing ensures the user interface remains consistent and visually appealing, helping teams catch issues early and streamline development.

Visual regression testing ensures visual integrity by detecting unintended changes in the application's appearance, layout, and design, maintaining a high-quality user experience across devices and updates.

The real work is:

  • Maintaining stability
  • Establishing a baseline discipline
  • Conducting a clean CI review

Playwright visual regression testing ensures visual integrity by detecting unintended changes in the application's appearance, layout, and design, maintaining a high-quality user experience across devices and updates.

FAQs

Run:
npx playwright test --update-snapshots
Review the git diff before committing.

Get started fast

Step-by-step guides, real-world examples, and proven strategies to maximize your test reporting success