Best Visual Regression Testing Tools (2026)
Playwright, BackstopJS, Percy, Chromatic, Applitools, Argos and DIY pixelmatch compared, with a flake test we ran and prices checked in October 2026.
Disclosure: RenderScreenshot sells a screenshot API. We do not sell a visual regression testing tool, and none of the tools below is ours. Our API appears once, at the end, as a capture source you can feed into a diff.
The best visual regression testing tool depends on where your UI lives. For component libraries in Storybook, Chromatic. For full pages inside an existing Playwright suite, Playwright's own toHaveScreenshot first, and Percy or Argos when you want hosted review. For URL lists without test code, BackstopJS. Applitools is the choice when you need AI-based matching and can pay enterprise prices.
How we got there: we ran three of the tools that run locally (Playwright Test, BackstopJS, and a DIY Playwright + pixelmatch script) 20 times each against one fixture page built to flake, then 10 times each against a version with a real layout regression. We read the documentation and pricing pages of the hosted tools on 2026-10-18. We did not run the hosted tools, because each needs an account and a project; their sections say so.
The short list
| Tool | Kind | Best for | Baselines live in | Free tier (checked 2026-10-18) |
|---|---|---|---|---|
Playwright Test toHaveScreenshot |
Open source test runner | Teams already on Playwright | Your repository | Free |
| BackstopJS | Open source CLI | URL lists, no test code | Your repository | Free |
| DIY: Playwright or Puppeteer + pixelmatch | Library code | Full control, custom pipelines | Wherever you put them | Free |
| Percy (BrowserStack) | Hosted review | Full-page tests from an E2E suite | Percy | 5,000 screenshots a month |
| Chromatic | Hosted review | Storybook component libraries | Chromatic | 5,000 snapshots a month |
| Argos | Hosted review, open source core | Playwright and Storybook teams who want a cheaper hosted option | Argos | Up to 5,000 screenshots a month (Hobby) |
| Applitools Eyes | Hosted, AI-based matching | Large suites across many browsers | Applitools | No free plan listed |
| Lost Pixel | Hosted and open source | Existing users only | Lost Pixel | Product is being sunset (see below) |
What we measured
A visual testing tool has two jobs that pull against each other. It must pass when nothing changed (no flakes), and it must fail when something did (no blind spots). Most reviews only score the first. We scored both.
The fixture. One static HTML page served from localhost, with four classic sources of flakiness:
- a timestamp printed with
new Date().toISOString(); - a carousel that changes color every 700 ms;
- a CSS spinner with an infinite animation (it stops under
prefers-reduced-motion: reduce); - a data block inserted after a random 200 to 1,200 ms, which pushes the footer down.
The regression. For the second run we changed one CSS rule: the data block's bottom padding went from 12 px to 48 px. A real bug of the kind visual tests exist to catch.
The configs. Each tool ran twice: once with defaults ("naive"), and once with the stabilization features its own documentation offers ("hardened"):
| Tool | Naive | Hardened |
|---|---|---|
| Playwright Test 1.64.1 | toHaveScreenshot() with defaults (animations are disabled by default) |
emulateMedia({ reducedMotion: 'reduce' }), wait for #data to be visible, mask the clock and carousel, maxDiffPixelRatio: 0.001 |
| BackstopJS 6.3.25 (Playwright engine) | URL only, misMatchThreshold: 0.1 |
readySelector: "#data", hideSelectors for clock, carousel and spinner |
| DIY: Playwright 1.64.1 + pixelmatch 7.1.0 | Screenshot after load, fail above 0.1% changed pixels |
reducedMotion: 'reduce' on the context, wait for #data, CSS that hides clock and carousel |
Method. macOS 27.0.1 on an Apple M1 Pro, Node 22.18.0, Chromium 156.0.8078.4 (Playwright's build), fixture on 127.0.0.1. Baselines taken once per config, then 20 runs on the unchanged page and 10 runs on the regressed page. Time is wall-clock time of the whole command, including browser start. Run on 2026-10-18. A local fixture has no network latency, so these times are lower than you will see against a real site.
Results
| Tool and config | Unchanged page: failed runs (flakes) | Regressed page: detected | p50 run time | p95 run time |
|---|---|---|---|---|
| Playwright Test, naive | 20 / 20 | 10 / 10 | 1.81 s | 2.10 s |
| Playwright Test, hardened | 0 / 20 | 10 / 10 | 2.50 s | 2.90 s |
| BackstopJS, naive | 0 / 20 | 0 / 10 | 1.06 s | 1.23 s |
| BackstopJS, hardened | 0 / 20 | 10 / 10 | 1.69 s | 2.21 s |
| DIY pixelmatch, naive | 0 / 20 | 0 / 10 | 0.55 s | 0.58 s |
| DIY pixelmatch, hardened | 0 / 20 | 10 / 10 | 1.37 s | 1.87 s |
Run times are for the unchanged-page runs; the regressed runs took up to 0.6 s longer at p95 and about the same at p50.
Three things in this table matter more than the times:
- Defaults fail in opposite directions. Playwright's default tolerance is zero, so the 230 pixels of the timestamp failed every run. BackstopJS and our DIY script allow 0.1% of pixels to differ, so the timestamp passed, and so did everything else.
- A green naive run can test nothing. BackstopJS and the DIY script captured at the
loadevent, before the data block existed. Their baselines show an empty slot. When we broke that block, they passed 10 of 10 times. This is worse than a flake, because nobody investigates a pass. - Hardening fixed both problems in every tool, at a cost of under a second a page. The work is the same everywhere: wait for the last element, hide or mask what moves, and turn motion off. The tool matters less than the discipline.
Raw data: results.csv and regression.csv from run-bench.sh in our test harness. The fixture, the six configs and the runner are reproduced in the last section of this article.
The checklist, scored
Each row is a question we would ask before adopting a tool. "Yes" means the documented feature exists; it does not mean we tested it, except for the three local tools.
| Question | Playwright Test | BackstopJS | DIY | Percy | Chromatic | Argos | Applitools |
|---|---|---|---|---|---|---|---|
| Runs with no account or token | Yes | Yes | Yes | No | No | No | No |
| Masks or hides dynamic regions | Yes (mask) |
Yes (hideSelectors) |
Your code | Yes | Yes | Yes | Yes (regions) |
| Waits for a selector before capture | Yes | Yes (readySelector) |
Your code | Through your tests | Through stories or tests | Through your tests | Through your tests |
| Review and approve in a browser UI | No (HTML report) | No (HTML report) | No | Yes | Yes | Yes | Yes |
| Baselines independent of your CI OS | No | No | No | Yes (renders remotely) | Yes (renders remotely) | No (your runner) | Yes (grid) |
| Tolerance besides pixel ratio | No | No | Your code | Yes | Yes | Yes | Yes (AI matching) |
| Free tier for a small project | Yes | Yes | Yes | Yes | Yes | Yes | No |
The row that decides most choices is the fifth. If baselines depend on your CI OS, you must pin and maintain that environment, and a font package update can fail every test. Hosted renderers move that work to the vendor, at a price per screenshot.
Pick by situation
| Situation | Pick | Why |
|---|---|---|
| You already have Playwright end-to-end tests | Playwright Test | Visual checks become one line in tests you already maintain |
| A design system in Storybook | Chromatic | Built around stories, with review per component |
| Marketing site or docs, no test code | BackstopJS | A JSON file of URLs is the whole setup |
| Several reviewers who approve changes in a browser | Percy or Argos | Hosted diff review, approval flow and GitHub checks |
| Many browsers and devices, large budget | Applitools Eyes | AI-based matching and a cross-browser grid |
| You need the capture inside another pipeline (monitoring, audits, PR comments) | DIY pixelmatch | The diff is 20 lines and you control everything around it |
Playwright Test
What it is. The @playwright/test runner ships expect(page).toHaveScreenshot(). The first run writes a baseline PNG next to the test; later runs compare and write expected, actual and diff images on failure. Baseline names include the browser and the platform, because text renders differently on macOS and Linux.
Strengths. No new service, no new account, baselines reviewed in pull requests like code. Masking with locators is precise. It waits for two identical screenshots in a row before it compares, which absorbs some late rendering.
Weaknesses. You own the baseline workflow: who updates them, on which OS, and how reviewers see diffs (the HTML report, not a review UI). Baselines from a laptop fail in CI; generate them in the same Docker image CI uses. Zero tolerance by default, as our naive run shows.
Price. Free and open source.
The hardened test we ran:
// hardened.spec.mjs: Playwright Test 1.64.1 import { test, expect } from '@playwright/test'; test('hardened', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }); await page.goto('http://127.0.0.1:4020/'); await expect(page.locator('#data')).toBeVisible(); await expect(page).toHaveScreenshot('hardened.png', { mask: [page.locator('#clock'), page.locator('.carousel')], maxDiffPixelRatio: 0.001, }); });
Run npx playwright test --update-snapshots once for the baseline. Passed 20 of 20 on the unchanged fixture and failed 10 of 10 on the regression.
BackstopJS
What it is. A CLI that reads a JSON config of scenarios (URL, viewports, selectors) and runs backstop reference to capture baselines and backstop test to compare, with an HTML report. It drives Puppeteer by default and Playwright as an option.
Strengths. No test code. A designer or QA engineer can add a URL to a list. readySelector, hideSelectors, removeSelectors and delay cover most stabilization needs, and the HTML report has a scrubber to compare before and after.
Weaknesses. No built-in way to emulate reduced motion; we hid the spinner instead. Engine scripts for logins or clicks are plain JavaScript files you maintain. Releases are less frequent than Playwright's, so check that the engine version you pin is current.
Price. Free and open source.
The hardened scenario we ran (the paths block is omitted for length):
{ "id": "hardened", "viewports": [{ "label": "desktop", "width": 1000, "height": 700 }], "scenarios": [ { "label": "pricing", "url": "http://127.0.0.1:4020/", "misMatchThreshold": 0.1, "readySelector": "#data", "hideSelectors": ["#clock", ".carousel", ".spinner"] } ], "engine": "playwright", "engineOptions": { "browser": "chromium" }, "report": ["CI"], "asyncCaptureLimit": 1 }
npx backstop reference --config=backstop.hardened.json, then npx backstop test --config=backstop.hardened.json. Passed 20 of 20 and failed 10 of 10 on the regression.
DIY: Playwright or Puppeteer with pixelmatch
What it is. Take the screenshot with any browser library, compare it with a stored PNG using pixelmatch, fail above a ratio. The pixel diff glossary entry explains the threshold.
Strengths. Fits anywhere: a scheduled monitor, a post-deploy job, a PR comment bot. You decide what counts as a change, how to handle different image heights, and where baselines live.
Weaknesses. You build everything a tool gives you for free: baseline updates, a report, a review step. And, as our naive run shows, nothing protects you from capturing too early.
Price. Free.
// diy.mjs: Playwright 1.64.1 + pixelmatch 7.1.0. Usage: node diy.mjs naive|hardened [--update] import { chromium } from 'playwright'; import pixelmatch from 'pixelmatch'; import { PNG } from 'pngjs'; import { readFile, writeFile } from 'node:fs/promises'; const mode = process.argv[2]; const update = process.argv.includes('--update'); const browser = await chromium.launch(); try { const context = await browser.newContext({ viewport: { width: 1000, height: 700 }, reducedMotion: mode === 'hardened' ? 'reduce' : 'no-preference', }); const page = await context.newPage(); await page.goto('http://127.0.0.1:4020/', { waitUntil: 'load' }); if (mode === 'hardened') { await page.locator('#data').waitFor(); await page.addStyleTag({ content: '#clock, .carousel { visibility: hidden !important; }' }); } const shot = await page.screenshot(); const file = new URL(`${mode}.base.png`, import.meta.url); if (update) { await writeFile(file, shot); } else { const a = PNG.sync.read(await readFile(file)); const b = PNG.sync.read(shot); const changed = a.height === b.height ? pixelmatch(a.data, b.data, null, a.width, a.height, { threshold: 0.1 }) : Infinity; if (changed / (a.width * a.height) > 0.001) { console.error(`${mode}: ${changed} px changed`); process.exitCode = 1; } } } finally { await browser.close(); }
visibility: hidden keeps the hidden elements' space, so nothing below them moves. display: none would shift the layout and hide a different set of regressions.
Percy
What it is. BrowserStack's hosted visual review. Your test suite sends DOM snapshots through an SDK (@percy/playwright, @percy/cypress, Storybook and others); Percy renders them in its own browsers, compares with the approved build and shows the diffs in a review UI with GitHub status checks.
Strengths. Reviewers approve changes in a browser, not in a pull request diff of PNG files. Rendering happens on Percy's side, so the local OS does not affect baselines. Works with the end-to-end suite you have.
Weaknesses. Snapshots are DOM plus assets, re-rendered remotely; content drawn by JavaScript at capture time, canvas and some cross-origin assets need care. Usage is metered per screenshot, and each browser and width counts.
Price. As listed on BrowserStack's pricing page on 2026-10-18: Free, 5,000 screenshots a month; Desktop, $199 a month billed annually for 10,000 screenshots. Check the page for current plans.
We did not run Percy for this article; it needs a project token.
Chromatic
What it is. A hosted service from the Storybook maintainers. It captures every story, compares with the accepted baseline, and gives each component a review and approval flow. It also supports Playwright and Cypress tests.
Strengths. The best fit for component libraries: diffs per story, per browser and per viewport, linked to the component. TurboSnap re-tests only stories that changed code touched, which saves snapshots.
Weaknesses. The value is highest when your UI is in Storybook. For full-page tests of a marketing site, a page-oriented tool is simpler.
Price. As listed on chromatic.com/pricing on 2026-10-18: Free, 5,000 snapshots a month; Starter, $179 a month with 35,000 snapshots; Pro, $399 a month with 85,000; extra snapshots $0.008 each.
We did not run Chromatic for this article; it needs a project token.
Argos
What it is. A hosted visual testing service with an open source core. SDKs for Playwright, Cypress, Puppeteer and Storybook upload screenshots taken by your own tests; Argos compares and shows the review in its UI and on GitHub.
Strengths. Lower list price than the other hosted options at similar volume. The screenshots come from your own runner, so what you see is what your test saw.
Weaknesses. Because screenshots come from your runner, your CI environment must stay pinned, as with Playwright Test. Smaller ecosystem than Percy or Chromatic.
Price. As listed on argos-ci.com/pricing on 2026-10-18: Hobby, free up to 5,000 screenshots a month; Pro, $100 a month with 35,000 screenshots, then $0.004 each ($0.0015 for Storybook screenshots).
We did not run Argos for this article; it needs a project.
Applitools Eyes
What it is. A hosted visual testing platform that compares with AI-based matching instead of raw pixels, plus an Ultrafast Grid that re-renders one capture across many browsers and devices.
Strengths. The matching levels ignore rendering noise such as antialiasing and can treat dynamic regions as layout only. That reduces the hide-and-mask work our results table shows. Broad SDK support across languages and frameworks.
Weaknesses. Price. And the matching is a model you configure and trust rather than a threshold you can read.
Price. As listed on applitools.com/pricing on 2026-10-18: Starter, $667 a month paid annually, with 1,000 page checkpoints or 100,000 component checkpoints; Professional and Enterprise prices are not published.
We did not run Applitools for this article.
Lost Pixel
Lost Pixel offered open source and hosted visual testing for pages, Storybook and Ladle. On 2026-10-18 its pricing page carried the banner "Lost Pixel is joining Figma. We are sunsetting the product." Do not start a new project on it. If you use it today, plan a move to Argos or Playwright Test, which take the same kind of input.
Where a screenshot API fits
None of the tools above needs a screenshot API. A capture API helps in one case: when the visual check is not a test suite but a pipeline step, such as a post-deploy check, a scheduled monitor or a before/after comment on a pull request, and you do not want to run browsers in that job. The API renders; the comparison stays in your code, the DIY pattern above. Our visual regression testing use case shows that pattern with production and a PR preview rendered in the same minute. RenderScreenshot has no hosted diff today.
How to run your own evaluation
Our harness is small enough to copy. You need the fixture page, the six configs shown above, and this runner:
#!/usr/bin/env bash # Runs each tool/config N times against the unchanged fixture and records exit code and wall time. # Baselines must exist first. Output: results.csv set -u N=${N:-20} cd "$(dirname "$0")" echo "tool,config,run,exit_code,seconds" > results.csv run() { # tool config dir cmd... local tool=$1 config=$2 dir=$3; shift 3 for i in $(seq 1 "$N"); do local start end start=$(python3 -c 'import time; print(time.time())') (cd "$dir" && "$@" > /dev/null 2>&1); local rc=$? end=$(python3 -c 'import time; print(time.time())') echo "$tool,$config,$i,$rc,$(python3 -c "print(round($end-$start,2))")" >> results.csv done } run playwright-test naive pw npx playwright test -c playwright.config.mjs naive.spec.mjs run playwright-test hardened pw npx playwright test -c playwright.config.mjs hardened.spec.mjs run backstopjs naive bs npx backstop test --config=backstop.naive.json run backstopjs hardened bs npx backstop test --config=backstop.hardened.json run diy-pixelmatch naive diy node diy.mjs naive run diy-pixelmatch hardened diy node diy.mjs hardened
Then make one real regression in the fixture, run it again, and count detections. Replace our fixture with your own worst page: the one with the carousel, the chat widget and the slow API. A tool that passes on your worst page with your hardening is the right tool for you. Two more checks we did not do and you should: run the suite on your CI runner instead of a laptop, and have two people approve one change in each hosted tool's review UI.
Changelog
- 2026-10-18: First version. Flake and detection runs for Playwright Test 1.64.1, BackstopJS 6.3.25 and Playwright + pixelmatch 7.1.0. Prices for Percy, Chromatic, Argos, Applitools and Lost Pixel checked on their pricing pages.
For the capture settings that keep any of these tools quiet, see the visual regression testing glossary entry. If your check runs after deploy rather than before merge, the post-deploy screenshot check is a better fit than any tool here. To try an API capture in a diff pipeline, get a free API key.
Related in this topic
- Visual Regression Testing API for CI Screenshots Use a screenshot API as the capture step of visual regression testing. Render production and the PR preview with the same settings, then diff them in CI.
- Post-Deploy Screenshot Check in GitHub Actions Verify a deployment with screenshots. Render key pages after each production deploy, fail on missing or forbidden text, and see each capture in the job summary.
- Website Design Audit Screenshots on One Board Capture every page of a site on desktop, tablet and phone, in light and dark mode, and lay the screenshots out on one HTML audit board for a design review.
- Screenshots of Preview Deployments in Every PR Wait for the preview deployment to serve the PR's commit, render key pages next to production, and post a before/after table in the PR. Tested scripts.