How to Monitor Website Screenshot Changes When the Site Uses Dark Mode
Catch dark-mode visual regressions with Playwright: set an explicit theme, keep captures stable, review baselines, and tune screenshot comparisons.
To monitor screenshot changes on a website in dark mode, render the page with the browser’s dark color scheme, save an approved screenshot as the baseline, then compare later captures against it. With Playwright Test, set colorScheme: 'dark' and use toHaveScreenshot(). Keep the browser and operating environment consistent, review each diff, and update the baseline only when the visual change is intentional.
This catches changes such as a dark background unexpectedly turning light, low-contrast text, altered spacing, or a component that ignores the theme. Playwright notes that screenshot rendering can vary with the operating system, browser version, settings, hardware, power source, and headless mode. [Playwright visual comparisons]
1. Decide which dark-mode states to monitor
Start with the pages and states where dark-mode appearance matters. A useful capture matrix may include a small set of representative pages, viewports, and interaction states. Each distinct screenshot assertion can have its own reference image, so keep the matrix focused on states that could reveal a real regression.
- List critical routes, such as a dashboard, sign-in page, or checkout flow.
- Choose viewport sizes that cover your supported layouts.
- Include important states such as an open menu, validation message, or expanded panel.
- Decide whether you are checking the system-preference theme, a manual theme toggle, or both.
Playwright’s dark color-scheme emulation selects the browser media preference. A site may also have a manual toggle that stores its setting separately. If so, use the application’s own control or setup mechanism to put it in the intended state before taking the screenshot; media emulation alone does not guarantee that a site-specific toggle is enabled. [Playwright page.emulateMedia()]
2. Set up Playwright and create a dark-mode baseline
Install Playwright Test in an existing Node.js project, or create a new project using the official setup instructions. The following example assumes Playwright Test is installed and configured. [Playwright installation]
npm init playwright@latest
Set the theme in the Playwright configuration so tests in the project use dark mode by default. This example also fixes the viewport and uses a local example route; replace the route with a page in your application.
// playwright.config.js
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'http://127.0.0.1:3000',
colorScheme: 'dark',
viewport: { width: 1440, height: 900 },
},
});
Create a visual test that waits for the page to be ready and asserts the screenshot:
// tests/dark-mode.spec.js
import { test, expect } from '@playwright/test';
test('dashboard dark-mode appearance', async ({ page }) => {
await page.goto('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
await expect(page).toHaveScreenshot('dashboard-dark.png', {
fullPage: true,
});
});
Run the test once to create its reference image, then inspect and commit that baseline with the test project. Subsequent runs compare new screenshots with the reference. Playwright documents updating snapshots with --update-snapshots; use that after reviewing an intentional change, not as an automatic response to a failed comparison. [Playwright visual comparisons]
npx playwright test tests/dark-mode.spec.js
# After reviewing an intentional visual change:
npx playwright test tests/dark-mode.spec.js --update-snapshots
3. Select dark mode in the right place
There are several supported ways to set the color scheme. Prefer project or test configuration when the state is part of the test’s identity; use per-page emulation when one test needs to switch between themes.
Project-wide configuration
use: {
colorScheme: 'dark',
}
One test or one browser context
test('dark preference', async ({ browser }) => {
const context = await browser.newContext({ colorScheme: 'dark' });
const page = await context.newPage();
await page.goto('http://127.0.0.1:3000');
await expect(page).toHaveScreenshot('home-dark.png');
await context.close();
});
Change the preference on an existing page
await page.emulateMedia({ colorScheme: 'dark' });
await page.goto('http://127.0.0.1:3000');
await expect(page).toHaveScreenshot('home-dark.png');
Playwright documents light and dark as color-scheme values. Setting the preference makes browser media queries such as prefers-color-scheme: dark resolve accordingly. [Playwright page.emulateMedia()]
4. Control what the screenshot assertion compares
toHaveScreenshot() accepts options that help define the capture and the comparison. Choose them deliberately: a permissive threshold can hide a real regression, while a strict comparison can flag harmless rendering noise. See the current Playwright snapshot assertion options for the full API and defaults.
| Option or technique | Use it for | Watch out for |
|---|---|---|
fullPage |
Capturing the full scrollable page instead of just the viewport. | Long pages may contain more lazy content and dynamic regions to stabilize. |
maxDiffPixelRatio |
Setting a tolerated proportion of differing pixels. | Raising it can allow meaningful changes to pass; calibrate with reviewed diffs. |
threshold |
Controlling the perceived color difference used for pixel comparison. | It affects color comparison, not whether a layout change is acceptable to your team. |
stylePath |
Injecting a stylesheet to hide or neutralize volatile content during capture. | Do not hide parts of the interface whose appearance you intend to monitor. |
For example, this assertion injects a stylesheet to suppress a timestamp that changes on every run. The file is part of the test project and applies only during screenshot capture:
/* tests/visual-stability.css */
.dynamic-timestamp {
visibility: hidden !important;
}
await expect(page).toHaveScreenshot('dashboard-dark.png', {
fullPage: true,
stylePath: 'tests/visual-stability.css',
maxDiffPixelRatio: 0.01,
});
The numeric tolerance above is an example configuration, not a universal recommendation. Review the generated diff and choose a value that reflects the noise your project accepts. Prefer removing a known source of nondeterminism over continually increasing the allowed difference.
5. Make dark-mode comparisons reliable
- Use the same capture environment. Keep the browser version, operating system, fonts, viewport, and headless or headed mode consistent between baseline creation and comparison where practical. Playwright warns that these conditions can affect rendering. [Playwright visual comparisons]
- Wait for meaningful readiness. Navigate to the intended URL, wait for a key element to be visible, and ensure application data and fonts have loaded before asserting the image.
- Stabilize animation. Use Playwright’s screenshot assertion controls such as
animations: 'disabled'when animations cause captures to differ. Check the assertion API documentation for current behavior and supported options. - Handle dynamic content narrowly. Use
stylePathor another documented screenshot option to suppress a volatile clock, rotating banner, or live value only when that region is outside the test’s purpose. - Separate theme baselines. Give light and dark states distinct screenshot names or test identities. Do not let a light-mode reference stand in for the dark-mode expectation.
- Review before updating. Inspect the actual screenshot and diff. A changed baseline is an approval of the new appearance, so confirm the change is intended before committing it.
If you test a manual theme control, drive it as a user would and assert the resulting state before capture. For example, locate the toggle by its accessible role and name, click it, and verify a page-specific theme indicator or class. The exact assertion depends on your site’s implementation; do not assume Playwright’s media preference controls a separate application setting.
6. Choose local comparisons or hosted visual review
Playwright’s built-in screenshot assertions keep the test and reference images in the project workflow. If a team needs shared cloud review, Chromatic documents a Playwright integration with cloud snapshot generation and pixel diffs, and Percy documents Playwright snapshots with capture options including full-page capture and ignored regions. These are vendor-documented capabilities; confirm current integration requirements, browser and theme coverage, review workflow, retention, plan limits, and pricing directly before choosing a service. [Chromatic Playwright integration] [Percy Playwright integration]
When comparing approaches, decide where baselines and approvals should live, how reviewers will inspect diffs, which browsers and viewports matter, how dynamic regions are handled, and what usage or access limits apply. This research does not establish current prices or plan limits for hosted products.
7. Capture a dark-mode screenshot with ScreenshotNeo
ScreenshotNeo is a website screenshot API and MCP server from Yorker Media. Its API takes a URL and returns an image or PDF. Use the ScreenshotNeo API documentation for request options. The request below captures a URL; check the docs for the current parameter that selects dark mode and use it when you need a dark-theme capture.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)
Node.js
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}`);
await import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
These examples capture a page on demand; they do not by themselves create a visual regression baseline or compare images. For a repeatable monitor, schedule your own capture and comparison workflow, retain an approved reference, and review changes. ScreenshotNeo supports custom CSS and JavaScript, viewport and device options, full-page capture, caching, and async jobs; consult the docs for supported parameters and behavior.
ScreenshotNeo removes cookie and consent banners, newsletter popups, and chat widgets before capture, and accepts the consent banner as a visitor. These steps can be turned off. Its billing rules exclude bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits; responses include X-Page-Verdict and X-Billed headers. It also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for AI agents. Plans include 1,000 screenshots a month free with no card, then paid plans starting at $5 for 3,000; every feature is on every plan.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The page still looks light. | The app uses a manual theme setting, or the page was captured before the theme preference took effect. | Set colorScheme: 'dark' before navigation. If there is a separate toggle, activate it and assert the app’s resulting state before the screenshot. |
| The screenshot assertion fails on every run. | The page includes changing data, animation, late-loading content, or unstable rendering conditions. | Inspect actual and expected images; wait for a stable page element, disable animation where appropriate, narrowly suppress volatile regions, and align capture environments. |
| Many pages change after a browser or OS update. | Rendering differences can be introduced by the environment. | Check whether the browser, OS, fonts, or headless mode changed. Review the diffs, then deliberately regenerate affected baselines if the new environment is the one your team will use. |
| A threshold lets an obvious defect pass. | The allowed pixel difference is too permissive for the page. | Reduce the tolerance and remove avoidable dynamic noise. Do not use a broad threshold to conceal changes in important components. |
| The captured page is incomplete. | The test took its screenshot before the page or relevant content was ready, or it captured only the viewport. | Wait for an application-specific readiness signal and use fullPage: true if the whole page is part of the check. |
| Baseline updates hide a regression. | Snapshots were updated without inspecting the diff. | Review each changed image and the code change that caused it. Update and commit only approved visual changes. |
| ScreenshotNeo returns an unexpected verdict or no useful image. | The page may have a bot check, be blank, time out, or fail to load. | Inspect the response’s X-Page-Verdict and X-Billed headers, then check the target URL and request options in the API docs. |
9. Performance, reliability, and cost
Visual checks cost time because they navigate pages, wait for content, and capture images. Keep the suite useful by prioritizing high-impact routes and theme states, reusing a stable test setup, and avoiding duplicate captures that add no coverage. Full-page captures can cover more content in one assertion, but require the entire page to settle and may include more dynamic regions.
For reliable comparisons, pin the browser version and use a consistent CI image when possible. Store approved reference images with the test project and make changes reviewable in version control. On failures, retain the actual, expected, and diff images so a developer can decide whether the change is a defect, harmless rendering variance, or an intentional redesign.
Playwright’s native workflow has no hosted visual-review pricing established by this guide; its operational cost is the time and infrastructure to run and maintain the tests. Hosted services may charge or impose usage limits, but verify current terms with each vendor. ScreenshotNeo’s stated tiers are Free: 1,000 shots/month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free. Only clean shots are billed, under the exclusions described above.
10. FAQ
Does dark color-scheme emulation change my operating system theme?
No. It sets the browser context’s emulated media preference for the page under test.
Should I keep dark and light screenshots in one test?
You can test both, but give each theme its own clearly named screenshot expectation so a change in one state is easy to review independently.
Can a screenshot diff prove the page is accessible?
No. It can reveal visual changes, but it does not replace accessibility checks for contrast, semantics, keyboard use, or assistive technology behavior.
Can I use a screenshot API as the visual regression system?
An API can produce the captures, but monitoring also needs a baseline, a comparison step, and a review process. Confirm that your capture request can represent the exact theme and state you need before building around it.
Or skip the browser setup
ScreenshotNeo makes a screenshot with one request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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 a month with no card, and paid plans start at $5 for 3,000. Read the API docs, then sign up free for 1,000 screenshots a month, with no card.


