ScreenshotNeo

BlogHow-to

How to Mask Elements in Playwright Snapshots

Use Playwright's mask option to hide dynamic regions in visual snapshots, with stable locators, visibility rules, troubleshooting, and runnable code.

By the ScreenshotNeo team1 October 20265 min read

Use the mask option with one or more Playwright locators when you capture a visual snapshot. Playwright paints each matched element’s bounding box with an overlay (pink by default), so changing values do not create false diffs.

import { test, expect } from '@playwright/test';

test('masks a dynamic account value', async ({ page }) => {
  await page.goto('https://example.com/account');
  await expect(page).toHaveScreenshot('account.png', {
    mask: [page.getByTestId('dynamic-account-value')],
  });
});

This is the visual assertion workflow in Playwright Test. The assertion takes screenshots until two consecutive captures are identical, then compares that stable image with the stored expectation. See the LocatorAssertions API and visual comparisons guide.

1. Choose what to mask

A mask should identify only pixels expected to vary: timestamps, random avatars, balances, rotating ads, or generated IDs. Use a narrow, stable locator.

Target Example Use when
Test hook page.getByTestId('live-price') A stable test ID exists.
Accessible region page.getByRole('status') The changing region has a useful role.
CSS selector page.locator('[data-random]') No role, label, or test ID fits.

2. Mask a full-page snapshot

import { test, expect } from '@playwright/test';

test('dashboard has stable layout', async ({ page }) => {
  await page.goto('https://example.com/dashboard');
  await page.getByRole('heading', { name: 'Dashboard' }).waitFor();
  await expect(page).toHaveScreenshot('dashboard.png', {
    fullPage: true,
    mask: [page.getByTestId('last-updated'), page.locator('.rotating-recommendation')],
  });
});

fullPage: true compares the complete scrollable page; omit it for the viewport only.

3. Mask a component snapshot

import { test, expect } from '@playwright/test';

test('order card ignores generated reference', async ({ page }) => {
  await page.goto('https://example.com/orders/123');
  const card = page.getByTestId('order-card');
  await expect(card).toHaveScreenshot('order-card.png', {
    mask: [card.getByTestId('order-reference')],
  });
});

locator.toHaveScreenshot() captures only the selected element, which keeps component regressions focused.

4. Change the overlay color

await expect(page).toHaveScreenshot('profile.png', {
  mask: [page.getByTestId('user-name')],
  maskColor: '#00AAFF',
});

The default overlay is #FF00FF. maskColor changes the fill; it does not stabilize or reveal the underlying value.

5. Mask several regions

const volatile = [
  page.getByTestId('clock'),
  page.getByTestId('request-id'),
  page.locator('.personalized-banner'),
];
await expect(page).toHaveScreenshot('home.png', { mask: volatile });

Every element matched by each locator is masked. Use first() or a specific filter when repeated matches are not intended.

6. Mask only visible matches

Invisible matches are masked too. Add a visibility constraint when hidden templates or off-canvas copies must remain outside the mask.

await expect(page).toHaveScreenshot('results.png', {
  mask: [page.locator('[data-dynamic]').filter({ visible: true })],
});

You can also scope the locator to a visible container. Check responsive layouts that render desktop and mobile copies simultaneously.

7. Make the page stable before masking

  1. Navigate and wait for a meaningful readiness locator.
  2. Use deterministic data, locale, timezone, and viewport.
  3. Wait for fonts and geometry-critical images.
  4. Disable animations or wait for a known state.
  5. Mask only the remaining volatile regions.
import { test, expect } from '@playwright/test';

test('stable invoice', async ({ page }) => {
  await page.goto('https://example.com/invoice/42');
  await page.getByRole('heading', { name: 'Invoice' }).waitFor();
  await page.evaluate(() => document.fonts.ready);
  await page.addStyleTag({ content: `*, *::before, *::after {
    animation: none !important; transition: none !important;
    caret-color: transparent !important;
  }` });
  await expect(page).toHaveScreenshot('invoice.png', {
    mask: [page.getByTestId('invoice-number')],
  });
});

A mask is not a substitute for waiting on data that controls layout.

8. Store and update baselines safely

Generate a baseline deliberately, review it, and commit it with the test. Update snapshots only for intentional UI changes.

npx playwright test tests/account.spec.ts --update-snapshots

Do not update snapshots automatically after a CI failure; that would turn a regression into a new expectation.

9. What masking does and does not cover

Handles Still required
Random text, IDs, timestamps, avatars, and other pixels inside matched boxes. Fixing layout shifts from late data, fonts, or images.
Multiple dynamic regions and invisible matches. Correct, narrow locators.
Visual screenshot assertions. Separate ARIA snapshot assertions.

ARIA snapshots represent accessibility structure and use toMatchAriaSnapshot; screenshot masking applies to visual screenshots.

10. Common errors and fixes

Symptom Cause Fix
Mask does nothing No match at capture time or pixels lie outside the box. Check await locator.count() and target the smallest changing element.
Stable content disappears Locator targets a parent or repeated wrapper. Narrow it with a test ID, filter, or descendant.
Hidden elements are covered Invisible matches are included. Constrain to visible content.
Snapshot still flakes Fonts, animations, network, or layout are unsettled. Wait for readiness, disable motion, await fonts, and fix test data.
Machine-to-machine diffs Browser, OS fonts, viewport, scale, or color scheme differ. Pin one project/container and set these values explicitly.
TypeScript rejects mask Wrong matcher or outdated package. Use Playwright Test’s toHaveScreenshot and update Playwright.
ARIA check has no mask ARIA snapshots are not pixel comparisons. Keep visual and ARIA assertions separate.

11. Performance, reliability, and cost

  • Performance: Locators resolve during capture; a few precise locators are inexpensive, while broad selectors repeated across many screenshots add work.
  • Reliability: Stable selectors and deterministic fixtures reduce false positives more than larger masks.
  • Parallel runs: Isolate test data when workers can change the same account, clock, or queue.
  • Reviewability: The colored overlay makes intentionally ignored regions visible in diffs.
  • Storage: Full-page images and multiple browser projects increase baseline size; keep meaningful contracts.

12. Or skip the browser setup

If you need a clean image of a URL rather than an assertion inside Playwright, ScreenshotNeo provides a single screenshot API request. It removes cookie and consent banners, newsletter popups, and chat widgets before capture. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and responses identify verdict and billing with X-Page-Verdict and X-Billed. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the ScreenshotNeo API documentation for options.

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}`);

1,000 screenshots each month are free with no card. Paid plans start at $5 for 3,000 screenshots, with every feature on every plan. Create a free ScreenshotNeo account.

FAQ

Can I mask text without masking its container?

Yes. Locate the smallest element containing the changing text; the overlay follows its bounding box.

Can one locator mask multiple elements?

Yes. Every match is masked. Use a specific locator when only one instance should be ignored.

Does masking alter the page?

No. It changes only the image used for comparison.

Should I use toMatchSnapshot?

Use toHaveScreenshot for rendered images, generic toMatchSnapshot for text or buffers, and toMatchAriaSnapshot for ARIA structure.

Can locator screenshots use masks?

Yes. Page and locator screenshot assertions accept mask; choose scope based on the regression you want to protect.