How to Compare Website Screenshots in GitHub Actions with Playwright
Set up Playwright screenshot assertions in GitHub Actions, review and update baselines, and reduce flaky visual diffs with a consistent CI environment.
Use Playwright Test’s expect(page).toHaveScreenshot() to compare a page capture with a reviewed reference image. Commit the reference image to your repository; GitHub Actions can then run the same test on each pull request and report visual differences. Stable results depend on using the same operating system, browser version, and relevant page state when creating and checking baselines.
1. What Playwright screenshot comparison does
Playwright’s screenshot assertion captures a page or locator and compares the pixels with an expected image. The first run usually finds no reference and writes an actual screenshot; inspect it, then commit it as the baseline. Later runs compare the new capture with that file. These assertions are provided by the Playwright Test runner.
This is a visual regression check, not a replacement for functional assertions. Keep checks for important behavior—such as a successful checkout confirmation—alongside screenshots, so a visually similar but broken page does not pass unnoticed.
2. Add a runnable visual test
Install Playwright Test if it is not already a project dependency, and commit the resulting package manifest and lockfile:
npm install --save-dev @playwright/test
npx playwright install
Create tests/homepage.spec.ts:
import { test, expect } from '@playwright/test';
test('homepage visual baseline', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png');
});
For the relative URL / to work, set a baseURL in the Playwright configuration or navigate to a complete URL. The example below starts a development server; replace the command and port with the ones used by your app.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
use: {
baseURL: 'http://127.0.0.1:4173',
...devices['Desktop Chrome'],
},
webServer: {
command: 'npm run preview -- --host 127.0.0.1',
url: 'http://127.0.0.1:4173',
reuseExistingServer: !process.env.CI,
},
});
If your application needs seeded data, authentication, or a separate API service, add that setup to your project’s test fixtures or CI startup steps. Use deterministic test data and a known application state so a baseline represents the intended page.
3. Create and review the baseline
- Run
npx playwright testlocally in the same operating-system and browser environment you plan to use in CI. - When Playwright reports a missing snapshot, locate the generated actual image and inspect it at its intended viewport size.
- Confirm that the page loaded correctly and that fonts, images, and test data are present.
- Add the generated snapshot directory to version control and commit the reviewed baseline with the test.
- Run the test again. It should compare against the committed reference.
Use meaningful names such as homepage.png or checkout-confirmation.png. Playwright includes test and browser project context in snapshot paths. If you test multiple browser projects, keep their reference images separate; browser rendering can differ.
For an intentional design change, regenerate references with:
npx playwright test --update-snapshots
Review the changed image files as carefully as source changes before committing them. Updating snapshots without inspecting them can turn an unintended regression into the new expected result.
4. Run the checks in GitHub Actions
A workflow needs to check out the repository, install the locked dependencies, install the matching Playwright browser and system dependencies, start the app as configured, and run the tests. Save the HTML report as an artifact so failures can be inspected from the workflow run.
name: Playwright visual tests
on:
push:
pull_request:
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Adjust the Node version, action versions, package manager, application startup, and test command for your repository. Keep the lockfile committed and install the browser version associated with the locked Playwright package. If the test runner does not start the app through webServer, add the appropriate startup and readiness checks before the test step.
For more controlled visual comparisons, Playwright documents using a container image. Pin a compatible image version and keep it aligned with the Playwright version in your lockfile. A hosted runner is simpler to maintain, while a pinned container can make the capture environment more repeatable.
5. Keep screenshots stable
Playwright advises: “For consistent screenshots, run tests in the same environment where the baseline screenshots were generated.” Operating system, browser version, settings, hardware, power source, and headless mode can affect rendering. Generate and update references in the same CI environment where practical, and keep the Playwright package and browser version fixed through the lockfile and installation step.
Playwright’s screenshot assertion waits until two consecutive screenshots are stable before comparing. It disables animations by default; finite animations are fast-forwarded and infinite animations are canceled for capture. This reduces some noise, but cannot make different machines or changing page content identical.
Control changing page content
- Use fixed fixtures, seeded records, and deterministic dates instead of live or randomly generated data.
- Wait for the relevant page state before capturing; avoid arbitrary sleeps unless a delay is genuinely part of the behavior under test.
- Use a screenshot stylesheet where volatile elements must be hidden or normalized. Review that rule: hiding content can also hide a real regression.
- Keep viewport, device scale, locale, timezone, and color scheme consistent with the expected rendering.
6. Choose page or locator screenshots
Use a full-page assertion when the layout of the entire page is under test:
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
});
Use a locator assertion when a specific component is the unit you want to protect. This limits unrelated page content in the comparison:
await expect(page.locator('[data-testid="pricing-card"]'))
.toHaveScreenshot('pricing-card.png');
Locator screenshot assertions also wait for consecutive stable screenshots. Prefer a stable test ID or similarly deliberate selector over a fragile positional selector.
7. Tune visual differences carefully
The matcher compares pixels. Adjust tolerances only after deciding whether a diff is a real UI change, dynamic content, or an environment mismatch. Large tolerances can hide the regression the test is meant to catch.
| Option | What it controls | When to use it |
|---|---|---|
threshold |
Per-pixel perceived color difference accepted by the matcher. | For small known rendering variation after checking the environment and page state. |
maxDiffPixels |
Maximum number of differing pixels. | When a small fixed number of pixels may vary. |
maxDiffPixelRatio |
Maximum proportion of differing pixels. | When an allowance should scale with screenshot dimensions. |
Options can be set on a particular assertion or in the Playwright configuration under expect.toHaveScreenshot for shared defaults. Keep exceptions local when only one known region needs special handling, and explain the reason in the test.
await expect(page).toHaveScreenshot('homepage.png', {
maxDiffPixels: 20,
});
For changing regions, Playwright supports a screenshot stylesheet through stylePath. Use it to normalize or hide known volatile content, then document what the rule suppresses and why. A blanket rule can make an important regression invisible.
8. Parallel runs and multiple browsers
Playwright supports sharding tests across GitHub Actions jobs and merging reports. Sharding reduces the time each job spends running tests, but every shard that creates screenshots must use the same OS, browser, and configuration. Keep baselines in version control and avoid having separate jobs update the same reference files.
When a project runs Chromium, Firefox, and WebKit, generate and review each project’s references in its matching environment. Do not assume a baseline captured by one browser is interchangeable with another.
9. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Snapshot is missing | This is the first run, the snapshot was not committed, or the test/project name changed. | Inspect the actual image, add the intended baseline, and commit it. Check snapshot paths when renaming tests or projects. |
| It passes locally but fails in Actions | Different OS, browser revision, fonts, viewport, headless settings, or test data. | Use the same environment to create and compare references; install browsers from the locked Playwright version and stabilize fixtures. |
| Every run has a different diff | Live data, timestamps, animation, delayed resources, or nondeterministic layout. | Fix the page state and data, wait for the meaningful ready condition, and narrowly normalize known volatile content. |
| Navigation times out or the page is blank | The app server did not start, the configured URL is wrong, or the page depends on an unavailable service. | Verify the server command and readiness URL, then make required test services and seed data available before running Playwright. |
| Browser executable or shared library is missing | CI installed the package but not its browser or system dependencies. | Run npx playwright install --with-deps after dependency installation. |
| Too many snapshot files appear | Different projects, platforms, or test names produce distinct snapshots. | Review the configured projects and snapshot naming; retain the references needed for the supported CI matrix. |
| Updating snapshots makes the test pass but seems wrong | The new image was accepted without reviewing the visual change. | Inspect the generated image and Git diff before staging or committing updated references. |
10. Performance, reliability, and cost
Screenshot comparison adds browser navigation, page rendering, and image comparison to the test run. Keep the suite focused on pages and components where a visual regression matters. Locator captures reduce unrelated page area; sharding can distribute tests across jobs, with the tradeoff of additional runner jobs and report coordination.
Reliability mostly comes from controlling the environment and page state. A pinned dependency lockfile, matching browser install, consistent runner or container, stable test data, and reviewed baselines make failures easier to interpret. Retain the report artifact for failed workflow runs so the team can inspect actual and expected output.
Playwright’s built-in assertions run as part of your test workflow; this setup does not require a separate screenshot comparison service. GitHub Actions usage and any storage or artifact costs depend on your repository and plan, so check your organization’s current limits and billing details.
Or skip the browser setup
If you need a clean screenshot outside a Playwright test, ScreenshotNeo is a website screenshot API and MCP server. A request can return a PNG, JPEG, WebP, or PDF, and its parameters include full-page capture and CSS selector element capture. See the API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts cookie or consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 shots.
Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Can I use toHaveScreenshot() without Playwright Test?
The screenshot assertion is part of Playwright Test’s runner and assertion API. Use that runner for this baseline workflow.
Should expected screenshots be committed?
Yes. They are the reviewed reference images the test compares against, so keep them in version control with the test changes.
Can I use one baseline for every browser?
Usually, each browser project needs its own reference because rendering differs. Let Playwright’s project context manage separate snapshots.
Should I increase the pixel tolerance until CI is green?
No. First identify whether the difference comes from a real product change, unstable content, or a different rendering environment; then use the narrowest justified tolerance or normalization.


