Skip to main content
Back to Blog
Guide
2026-06-08

Percy Visual Testing + Playwright Official 2026 Guide

Wire up @percy/playwright for visual regression testing. Cover percySnapshot, viewports, PERCY_TOKEN, browsers, ignore regions, CI integration, and baselines.

Percy Visual Testing + Playwright Official 2026 Guide

Percy is BrowserStack's hosted visual regression service, and @percy/playwright is the official integration that lets you snapshot your Playwright tests, upload the renderings to Percy, and review pixel diffs in a polished web UI. Visual testing complements your functional Playwright tests by catching the bugs that functional assertions miss: a CSS regression that breaks card layouts, an unintended font swap on the marketing site, a button that overlaps another element after a refactor. This guide covers the full @percy/playwright setup from installation through CI, including viewports, browser matrix, ignore regions, baselines, the Percy CLI, and the production patterns most teams converge on.

If you are searching for percy visual testing playwright documentation, this guide walks you through every API surface and configuration option needed to get a clean, reviewable visual diff pipeline running.

Key Takeaways

  • @percy/playwright exposes a single function: percySnapshot(page, name, options?). Call it inside any Playwright test to capture a snapshot
  • Percy renders snapshots in its cloud across browsers (Chromium, Firefox, Edge, Safari) and viewports (mobile, tablet, desktop) per project config
  • Authentication is via PERCY_TOKEN env var set in CI. Tokens are write-only and scoped to one project
  • Run tests under the Percy CLI wrapper: npx percy exec -- npx playwright test. The CLI manages session lifecycle, upload batching, and parallel sharding
  • Baselines are managed in the Percy UI: the first run becomes the baseline, subsequent runs diff against it; approve diffs to make them the new baseline

Why Visual Testing

Functional tests prove the right text appears in the DOM. Visual tests prove the user sees what you intended. A failing visual diff catches:

  • CSS regressions from refactored styles
  • Font fallback bugs when a custom font fails to load
  • Layout breakage from responsive media queries
  • Color contrast changes
  • Icon swaps
  • Hidden overflow or scrollbars
  • Z-index regressions

These are exactly the bugs functional tests miss because the DOM is still correct -- only the rendering changed.

Installing @percy/playwright

npm install --save-dev @percy/cli @percy/playwright @playwright/test

The three packages serve different roles:

  • @playwright/test -- Playwright runner
  • @percy/playwright -- the percySnapshot integration
  • @percy/cli -- the wrapper that manages Percy sessions and uploads

Verify:

npx percy --version
# @percy/cli 1.x

Getting a Token

Create a project at percy.io, copy the write-only PERCY_TOKEN from project settings, and set it in your CI environment. Tokens look like web_abc123def456.... Never commit them.

Locally:

export PERCY_TOKEN=web_abc123...

In GitHub Actions:

env:
  PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

Hello World

// tests/home.spec.ts
import { test, expect } from '@playwright/test';
import { percySnapshot } from '@percy/playwright';

test('homepage looks correct', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page.getByRole('heading', { name: /welcome/i })).toBeVisible();
  await percySnapshot(page, 'Homepage');
});

Run:

npx percy exec -- npx playwright test

Percy uploads the snapshot, runs it against the configured browser/viewport matrix, and writes a build URL to the console. Open the URL to review.

percySnapshot API

percySnapshot(page: Page, name: string, options?: PercySnapshotOptions): Promise<void>
OptionTypeDescription
widthsnumber[]Viewport widths to render at (overrides project default)
minHeightnumberMinimum render height
percyCSSstringCSS injected before snapshot (mask data, hide carousels)
enableJavaScriptbooleanRun JS in the rendered snapshot (default false for speed)
scopestringCSS selector to crop to a region
fullPagebooleanCapture full scrolling height (default true)

Common usage:

await percySnapshot(page, 'Product card', {
  widths: [375, 768, 1280],
  percyCSS: '.timestamp { visibility: hidden; }',
  scope: '.product-card',
});

.percy.yml Config

Project-wide defaults live in .percy.yml (or .percy.json):

version: 2
snapshot:
  widths: [375, 768, 1280]
  minHeight: 1024
  percyCSS: |
    .ads, .live-clock { visibility: hidden; }
discovery:
  allowed-hostnames: [example.com]
  disallowed-hostnames: [tracking.example.com]
  network-idle-timeout: 750

discovery.allowed-hostnames whitelists which assets Percy should fetch when rendering. Leave empty to fetch everything; restrict for faster builds and reproducible diffs.

Viewports

The most useful Percy feature: render the same snapshot at multiple widths and diff them independently. Configure in .percy.yml or per-snapshot:

await percySnapshot(page, 'Checkout', { widths: [375, 768, 1280, 1920] });

Each width produces a separate render in the Percy UI. Diffs are computed per width. A regression that only appears at mobile shows up only at the 375px width and stays green at 1280px.

Browser Matrix

In project settings (percy.io UI) you choose which browsers to render in:

BrowserNotes
ChromiumDefault, fastest
FirefoxGood cross-engine coverage
EdgeWebKit-on-Chromium
SafariReal WebKit, important for Apple-heavy audiences

More browsers = more snapshots = more cost. Most teams enable Chromium + Firefox in CI and add Safari for releases.

Ignore Regions

Some elements should not participate in diffs -- a timestamp, an ad slot, a randomly chosen featured product. Hide them with percyCSS:

await percySnapshot(page, 'Dashboard', {
  percyCSS: \`
    .live-timestamp { visibility: hidden; }
    [data-test='ad-slot'] { background: #fff !important; }
    .featured-product img { opacity: 0; }
  \`,
});

Use visibility: hidden (preserves layout) rather than display: none (collapses layout, often causes other diffs).

Comparison: Percy vs Other Visual Tools

ToolStrengthsWeaknesses
PercyCloud rendering, polished UI, BrowserStack-backedPaid SaaS, limited free tier
ChromaticNative Storybook integration, TurboSnapStorybook-centric
ApplitoolsAI-powered visual AI, large enterprise install basePricey, complex
Playwright toHaveScreenshotBuilt into Playwright, freeNo cloud, no review UI, local-only
Backstop.jsFree, self-hosted, good for static sitesSelf-managed infra

Percy fits teams that want managed infrastructure plus a clean review UI without the Storybook-first commitment.

CI Setup -- GitHub Actions

name: Visual Tests
on: [pull_request]
jobs:
  visual:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install
      - run: npx playwright install --with-deps chromium
      - name: Run Percy
        run: npx percy exec -- npx playwright test
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
          PERCY_BRANCH: ${{ github.head_ref || github.ref_name }}

The PERCY_BRANCH env var makes Percy associate the build with the right branch -- important for cross-PR baselines.

Pull Request Status Check

Percy posts a check to GitHub PRs when configured. The check reports:

  • Build status (pending, finished)
  • Number of diffs
  • Direct link to the visual review UI

In GitHub branch protection, add "Percy" as a required check to block merges until visual diffs are approved.

Baselines and Approval Flow

The first time you snapshot a page, that rendering becomes the baseline for the branch. Subsequent runs compare against the baseline. Three outcomes:

  1. No diff: snapshot matches baseline. No action.
  2. Diff in PR: reviewer approves or rejects in Percy UI. Approval makes the new render the baseline after merge.
  3. Unrelated diff: snapshot changed because of an upstream merge. Auto-resolves when you rebase.

Baselines live per branch. When a branch merges to main, its baselines become the new main baseline.

Authenticated Pages

To snapshot authenticated pages, log in inside Playwright as normal:

test.beforeEach(async ({ page }) => {
  await page.goto('https://app.example.com/login');
  await page.fill('input[name=email]', process.env.TEST_EMAIL!);
  await page.fill('input[name=password]', process.env.TEST_PASSWORD!);
  await page.click('button[type=submit]');
  await page.waitForURL('**/dashboard');
});

test('dashboard layout', async ({ page }) => {
  await percySnapshot(page, 'Dashboard');
});

Percy renders the page snapshot from the HTML+CSS as seen at the moment of percySnapshot(). It does not re-execute your auth flow on its servers.

Snapshotting Mid-State (Modals, Hovers)

To capture interactive states, drive the state in Playwright first:

test('settings modal', async ({ page }) => {
  await page.goto('/profile');
  await page.click('button:has-text("Edit settings")');
  await page.waitForSelector('.modal');
  await percySnapshot(page, 'Settings modal');
});
test('button hover state', async ({ page }) => {
  await page.goto('/');
  await page.hover('button.cta');
  await percySnapshot(page, 'CTA hover');
});

Multiple Snapshots Per Test

A single test can capture many snapshots:

test('three-step wizard', async ({ page }) => {
  await page.goto('/wizard');
  await percySnapshot(page, 'Wizard step 1');

  await page.click('button:has-text("Next")');
  await percySnapshot(page, 'Wizard step 2');

  await page.click('button:has-text("Next")');
  await percySnapshot(page, 'Wizard step 3');
});

Each snapshot is a separate item in the Percy UI with its own diff history.

Parallel Sharding

When your test suite shards across machines, Percy needs to know when all shards have uploaded. Use PERCY_PARALLEL_TOTAL and PERCY_PARALLEL_NONCE:

jobs:
  visual:
    strategy:
      matrix:
        shard: [1, 2, 3, 4]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm install
      - run: npx playwright install --with-deps chromium
      - run: npx percy exec -- npx playwright test --shard=${{ matrix.shard }}/4
        env:
          PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
          PERCY_PARALLEL_TOTAL: 4
          PERCY_PARALLEL_NONCE: ${{ github.run_id }}

Percy waits for all 4 shards to upload before finalizing the build.

Percy CLI Reference

CommandPurpose
percy exec -- <cmd>Wrap a command in a Percy session
percy snapshot <dir>Snapshot a static directory of HTML files
percy upload <dir>Upload pre-captured images
percy build:createManually start a build (advanced)
percy build:finalizeManually finalize a build
percy stopStop a running Percy session

percy exec is the most common; the others are for advanced workflows.

Reducing Flakiness

CauseFix
AnimationsInject * { animation: none !important; transition: none !important; } via percyCSS
Late-loading fontsWait with await page.evaluate(() => document.fonts.ready)
Dynamic timestampsHide with visibility: hidden
CarouselsPause autoplay before snapshot
Random dataSeed your test data
Network latencyUse networkIdleTimeout in .percy.yml

A defensive percyCSS template:

snapshot:
  percyCSS: |
    * { animation: none !important; transition: none !important; }
    .timestamp, .live-counter, .ads { visibility: hidden; }

Comparison Table -- Percy Plans

PlanSnapshots/monthConcurrencyApprox price
Free5,0001$0
Standard25,000+1starts ~$199/mo
Pro200,000+unlimitedenterprise

Pricing changes; check percy.io for current. The free tier is enough for small projects; teams shipping daily quickly outgrow it.

When Not to Use Percy

  • Pure backend services with no UI
  • Sites with extreme legitimate visual churn (news homepages, social feeds)
  • Compliance-restricted environments that cannot upload screenshots externally
  • Solo projects where cost outweighs value

For self-hosted alternatives see Backstop.js or Playwright's built-in toHaveScreenshot with explicit baseline files.

Frequently Asked Questions

Does Percy run my JavaScript?

By default Percy snapshots are static -- the HTML+CSS+images at the moment of capture. Set enableJavaScript: true in your snapshot options to have Percy re-execute the JS during rendering. JS-enabled snapshots are slower and more expensive but accurate for client-rendered apps.

What is the difference between widths and percyConfigurations?

widths is per-snapshot or project-default viewport widths. percyConfigurations (in Percy UI) defines the browser+viewport matrix per project, overriding per-snapshot widths. Most teams use widths in code and let project config handle browsers.

How do I handle a redesign that breaks every snapshot?

Approve all diffs in bulk via the Percy UI after the redesign ships. The new renders become the baselines. Alternatively, run the new branch separately and merge baselines manually.

Can I run Percy snapshots without uploading?

Yes -- set PERCY_TOKEN= (empty) and Percy CLI runs in dry mode, logging what it would have uploaded. Useful for development.

Does Percy work with mobile native apps?

Yes -- @percy/appium integrates with Appium for iOS and Android visual testing. Pattern is similar to the Playwright integration.

How do I share baselines across branches?

Percy auto-promotes the latest main-branch baseline. For other shared baselines, configure PERCY_TARGET_BRANCH to point to the reference branch.

What if my CI doesn't support env secrets?

Don't use Percy from that CI. PERCY_TOKEN is write-only but still grants snapshot upload privileges; treat it like an API key.

Conclusion

@percy/playwright adds visual regression testing to your existing Playwright suite with one extra function call and a CLI wrapper. The first integration takes an hour: install, set the token, add percySnapshot to a critical-path test, run under percy exec. From there it scales naturally -- add snapshots to flows that have visual risk, configure percyCSS to mask volatile content, and gate PRs on Percy approval.

For more visual testing guides, see our Chromatic Storybook + TurboSnap guide for the Storybook-centric alternative, browse the visual testing skills for AI agent skills that scaffold percy configs, and our visual testing comparison to evaluate Percy, Chromatic, and Playwright's built-in screenshot diffing side by side.