How to Compare Screenshots of a Website with Dynamic Text Content
Compare changing website content without hiding real visual bugs. Stabilize test data, mask only irrelevant text, and keep content checks separate.
To compare website screenshots when text changes between loads, first decide whether that text is part of the behavior being tested. If it is irrelevant volatility, mask or hide only that small region. If the words, value, wrapping, or resulting layout matter, use deterministic test data and keep a content assertion alongside the screenshot comparison. A visual diff is meaningful only when the captured page state is repeatable.
This guide uses Playwright’s built-in screenshot assertions. They wait for two consecutive screenshots to match before comparing against the baseline, and provide options for animations, locator masks, screenshot-only stylesheets, and comparison tolerances. See the PageAssertions API and visual comparisons guide for version-specific details.
1. Decide what the comparison must prove
Write down the test question before adding a mask. A screenshot assertion can help detect visual changes, but it does not by itself prove that changing text contains the correct data.
| What matters | Recommended approach | What the screenshot check tells you |
|---|---|---|
| A timestamp or other incidental value changes | Mask or hide only its element; retain other checks for the page | Whether the unmasked visual appearance changed |
| The text value or exact wording is important | Control the fixture or mock the data, then assert or snapshot the text | Whether the rendered appearance matches the baseline; the content check verifies the value |
| Text styling and content both matter | Use deterministic values and a screenshot assertion | Whether both the expected content and visual presentation are present, through separate checks |
| Text length, wrapping, or container size matters | Use representative controlled values and compare the relevant component without masking it | Whether the chosen values produce the expected geometry and appearance |
| A live feed or third-party result is volatile | Mock or seed it when feasible; otherwise isolate the inherently volatile region | Whether the owned interface changed outside the isolated region |
A mask covers the selected locator’s bounding box. If it overlaps adjacent content, it can hide a real defect. Masks also make changes inside the masked area invisible to the image comparison, so retain a functional or text check when that content matters.
2. Make the captured page repeatable
- Use the same route, viewport, browser project, locale, timezone, and relevant application state for the baseline and subsequent runs.
- Seed test data or mock APIs so dynamic text has a known value. Avoid relying on a live feed for a baseline unless that feed is the behavior under test.
- Wait for a meaningful readiness condition, such as the result component becoming visible or a loading indicator disappearing. Avoid using a fixed sleep as the only readiness check.
- Disable or finish animations when motion is not part of the test. Playwright screenshot assertions disable CSS animations, CSS transitions, and Web Animations by default; check the installed version’s API for exact behavior.
- Capture the smallest useful area. A component screenshot reduces unrelated changes elsewhere on a page; use a full-page capture only when the full page is the subject of the test.
- Review any baseline change before accepting it. A changed screenshot can be an intended design update or a regression.
Playwright’s screenshot assertion waits for two consecutive screenshots to produce the same result before comparing the final image with its expected snapshot. This helps with capture stability, but it does not make changing server data deterministic. Stabilize the data separately.
3. Runnable Playwright example: mask incidental text
The following JavaScript example assumes an existing Playwright Test project, a page with a [data-testid="updated-at"] element, and an installed browser. It checks the page’s visual appearance while masking only the volatile timestamp.
import { test, expect } from '@playwright/test';
test('dashboard appearance ignores its changing update time', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('http://127.0.0.1:3000/dashboard');
const heading = page.getByRole('heading', { name: 'Dashboard' });
await expect(heading).toBeVisible();
await expect(page.getByTestId('summary-card')).toBeVisible();
await expect(page).toHaveScreenshot('dashboard.png', {
fullPage: true,
mask: [page.getByTestId('updated-at')],
animations: 'disabled',
});
});
On the first run, Playwright may create the expected snapshot. Review that image and commit it as the baseline only when it represents the intended page. On later runs, the assertion compares the capture against that baseline.
The first run should also check that the test actually locates the intended volatile element. A selector that matches nothing, matches multiple unrelated elements, or has a larger bounding box than expected can make the comparison misleading. Prefer a stable test ID or an accessible locator tied to the specific component.
4. Use a screenshot-only stylesheet when masking is not enough
A screenshot assertion can apply a stylesheet to the capture. This can hide one volatile element or replace its displayed value for screenshot purposes, while leaving the page’s normal runtime behavior available to other assertions. Keep the style narrow and make clear in the test that the screenshot is not verifying the hidden value.
import { test, expect } from '@playwright/test';
test('profile layout with update time hidden in the screenshot', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('http://127.0.0.1:3000/profile');
await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();
await expect(page).toHaveScreenshot('profile.png', {
fullPage: true,
style: '[data-testid="updated-at"] { visibility: hidden !important; }',
animations: 'disabled',
});
});
visibility: hidden preserves the element’s layout space. If the changing text itself alters width or height and those geometry changes are important, hiding it may not answer the test question. Use controlled representative values and compare the component instead. If you use display: none, the screenshot no longer checks the element’s layout contribution.
5. Keep text verification separate when content matters
If the changing content is functional, make it deterministic and assert it directly. Playwright supports non-image snapshots, including text snapshots, as well as ordinary assertions. The exact snapshot API depends on the installed Playwright version; a regular assertion is straightforward and avoids treating a volatile live value as a visual baseline.
import { test, expect } from '@playwright/test';
test('dashboard displays the seeded total and expected appearance', async ({ page }) => {
await page.route('**/api/summary', async route => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify({ total: 42, updatedAt: '2026-01-15T12:00:00Z' }),
});
});
await page.setViewportSize({ width: 1280, height: 800 });
await page.goto('http://127.0.0.1:3000/dashboard');
await expect(page.getByTestId('total')).toHaveText('42');
await expect(page).toHaveScreenshot('dashboard-seeded.png', {
fullPage: true,
animations: 'disabled',
});
});
Adapt the route pattern and response body to the application’s API contract. If the application formats values according to locale or timezone, set those consistently too. Avoid asserting a mocked response if the test is intended to validate the live integration; use separate test coverage for that behavior.
6. Configure tolerances with care
Playwright supports screenshot comparison options for maximum differing pixels or ratio and a per-pixel color threshold. These are explicit tolerance choices: they can help account for small rendering differences, but they cannot make unstable data safe to ignore or distinguish an intended change from a bug.
- Maximum differing pixels or ratio: sets how much of the image may differ before the assertion fails. Keep the permitted difference as low as the application and rendering environment allow.
- Per-pixel color threshold: controls how different a pixel’s color can be and still count as a match. Raising it makes subtle color changes less likely to fail the comparison.
- Mask: covers specific locator bounds in the screenshot. Use only for content outside the visual test’s purpose.
- Screenshot stylesheet: changes the capture presentation for the screenshot only. Keep selectors scoped to the volatile part.
- Animation handling: disables animations by default for screenshot assertions in documented Playwright behavior. Confirm the installed version and options if the animation itself is under test.
- Capture scope: use a component locator, clipped region, or full-page option according to what needs checking. Avoid comparing irrelevant page areas.
Consult the PageAssertions API for option names supported by your installed version. The visual comparison documentation cited here is on Playwright’s next channel, so verify version-sensitive details before relying on them in a pinned project.
7. Playwright or layout matching?
Playwright keeps screenshot and text assertions in the project’s test workflow. A hosted visual-testing service such as Applitools Eyes is another approach when a team wants its visual review workflow and region-based handling. Applitools’ materials describe ignored regions and layout matching for dynamic content. Its guide cautions that layout matching does not verify the actual UI data, so pair layout checks with functional or data assertions. Product labels and availability may change; confirm current details in the vendor’s documentation.
Choose based on the test question and workflow: whether exact text must be checked, how narrowly the volatile area can be isolated, whether you want in-runner assertions or a hosted review workflow, and how reviewers handle baseline changes. The source material does not establish current pricing or service terms for Applitools.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot fails on every run around a timestamp or random value | Volatile data is included in the compared pixels | Seed or mock the data if it matters; otherwise mask or hide only that element. |
| The mask hides neighboring content | The selected locator’s bounding box is too large or the locator targets a parent container | Target the smallest specific element, inspect the screenshot and locator, and retain a separate assertion for affected content. |
| The page is still captured while loading | Navigation completed before the application reached its intended rendered state | Wait for a meaningful component or state indicator with an assertion before taking the screenshot. |
| The baseline changes across machines | Viewport, browser, fonts, locale, timezone, data, or rendering environment differs | Align those inputs and use the same browser project and stable fixtures for baseline generation and comparison. |
| A screenshot-only hide causes the component to shift | The stylesheet removes the element from layout, or the element’s size depends on its content | Use visibility: hidden to preserve space where appropriate, or control text length and test layout with representative values. |
| Text is visually present but its value is wrong | The screenshot test checks pixels, not data correctness | Add a direct text assertion or a text snapshot; keep the screenshot assertion for styling and layout. |
| A relaxed threshold passes a meaningful visual defect | The allowed pixel or color tolerance is too broad | Reduce tolerance and stabilize the capture inputs. Review the diff instead of tuning the threshold to make failures disappear. |
| The screenshot option is rejected or behaves differently | The project’s Playwright version differs from the documentation channel or example | Check the installed version’s API documentation and use options available in that version. |
| A new baseline appears without a clear reason | The baseline was generated or updated without review | Inspect the changed image and decide whether the visual change is intended before accepting it. |
9. Performance, reliability, and maintenance
Screenshot comparisons cost time in browser startup, page readiness, rendering, and image comparison. Keep tests focused on the interface behavior they protect: component captures can reduce the area under comparison, while full-page captures are appropriate when page-wide composition matters. Do not add arbitrary waits to make tests appear stable; wait for application state, and control data at its source where possible.
For reliability, keep the browser project, viewport, fixtures, and baseline review process consistent. A stable screenshot assertion does not guarantee a stable application response, and tolerances do not repair nondeterministic state. When a baseline changes, treat it as a reviewable code change. Maintain a separate test for live or external integrations if those are part of the product contract.
For cost, Playwright’s built-in screenshot assertions run as part of your browser test workflow; the sources here establish no separate Playwright screenshot-comparison charge. A hosted service can have its own pricing and terms, which should be checked directly before adoption. The appropriate scope is the least expensive workflow that still gives the needed repeatability and review process; no benchmark or price comparison is established by the research for this article.
10. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A one-call capture can help when you need a screenshot without wiring up a browser in your own script. It returns a PNG, JPEG or WebP image, or a PDF. It is a capture option, not a replacement for deterministic test fixtures or text assertions in a visual regression suite.
For a simple capture, save the response as an image:
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}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
See the ScreenshotNeo API documentation for request options and response details. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 screenshots.
Sign up for ScreenshotNeo’s free plan to get 1,000 screenshots a month with no card.
FAQ
Should I mask changing text in every visual test?
No. Mask it only when its exact value and appearance are outside the test’s purpose. Otherwise control the value and assert it.
Does a stable screenshot prove the page shows correct data?
No. A screenshot checks rendered pixels. Assert important text or data separately.
Can I compare a page that includes live third-party content?
Mock or seed it when feasible. If it cannot be controlled, isolate only the volatile region and keep checks for the rest of the page.
Should I update a baseline whenever the comparison fails?
Only after reviewing the change and deciding it is expected. A failure can indicate a regression.


