ScreenshotNeo

BlogHow-to

How to Test a Website’s Dark Mode with Screenshot Comparisons

Use Playwright to render dark mode, save screenshot baselines, and catch visual regressions with a repeatable comparison workflow.

By the ScreenshotNeo team4 October 20267 min read

To test a website’s dark mode with screenshot comparisons, render the same page states in a browser that emulates prefers-color-scheme: dark, save screenshots as visual baselines, and compare later runs against them. Review each flagged difference before updating a baseline. Keep the browser and host environment consistent so rendering changes do not drown out real regressions. Playwright supports both color-scheme emulation and screenshot assertions in Playwright Test (emulation; visual comparisons).

1. Set up Playwright visual comparisons

The examples use Playwright Test. In a project that does not already have it, install the test package and its browser using the official installation instructions. Keep the Playwright version and browser installation consistent between baseline creation and comparison runs.

npm init playwright@latest

For a TypeScript test, save the following as tests/dark-mode.spec.ts. It emulates dark mode for the test and asks Playwright to compare the page against landing-dark.png. The first run creates the reference; subsequent runs compare against it.

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

test.use({ colorScheme: 'dark' });

test('landing page dark appearance', async ({ page }) => {
  await page.goto('/');
  await expect(page).toHaveScreenshot('landing-dark.png');
});

Set baseURL in Playwright’s configuration if you want to use relative paths such as /. Otherwise, navigate to a full URL, for example https://your-site.example/. Use a separate name for each page and meaningful state so one reference cannot be mistaken for another.

2. Choose pages and states worth covering

Start with pages that represent important layouts and components, then capture repeatable interaction states. A compact set might include:

  • The landing page and a content page.
  • Navigation both closed and open.
  • A dialog, form, and validation error state.
  • An empty state and an error state.
  • Any theme toggle, if the site has one.
  • Focus, hover, disabled, and other interactive states where their appearance matters.

This inventory is a practical test-design choice, not a prescribed Playwright list. The aim is to compare equivalent content, viewport dimensions, and interactions in each run. Name references by page, scheme, and state, such as checkout-dark-error.png or nav-dark-open.png.

3. Request dark mode and capture stable baselines

Playwright supports the light and dark color schemes. You can set the scheme for tests with test.use, set it when creating a browser context, or change it on a page with page.emulateMedia(). See the Playwright emulation guide for the documented options.

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

test('settings page in dark mode', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' });
  await page.goto('/settings');
  await expect(page).toHaveScreenshot('settings-dark.png');
});

For the corresponding light-mode check, change the scheme and use a distinct baseline name. Keep the two captures matched by page, viewport, content, and interaction state. This helps identify a theme-specific defect without overlooking a light-mode regression.

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

test('settings page in light mode', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'light' });
  await page.goto('/settings');
  await expect(page).toHaveScreenshot('settings-light.png');
});

If your application has an in-page theme toggle, test that separately too. Media emulation requests a browser color scheme; it does not click the site’s toggle for you. Choose the interaction that real users rely on and reproduce it in the test before capturing.

4. Keep screenshot comparisons reproducible

Rendering can vary with the host operating system, browser version and settings, hardware conditions, and headless mode. Playwright recommends using the same environment for creating and comparing screenshot references because these differences can cause image changes unrelated to application code (Playwright visual comparisons).

  • Run baseline creation and later comparisons in the same operating system and browser setup.
  • Use the same viewport and device scale settings for corresponding captures.
  • Wait for the page to reach its intended state before capturing: complete navigation, open the desired menu or dialog, and populate form data consistently.
  • Make time, data, animations, and other volatile content deterministic where possible.
  • For content that cannot be stabilized, use supported screenshot controls or a screenshot style to manage it deliberately. Do not raise comparison tolerance so far that it hides meaningful visual defects.

Playwright’s screenshot assertion options and custom screenshot styles are documented in its screenshot comparison guide. Prefer fixing unstable test data over masking broad areas of a page.

5. Review diffs before accepting a baseline

When a comparison flags a change, inspect the changed region in the context of the full page. In dark mode, check:

  • Text legibility and contrast against the surrounding surface.
  • Surface colors and component boundaries, including borders that disappear or become too strong.
  • Icons and illustrations that keep an unsuitable light-theme color.
  • Images and embedded content that clash with the dark palette.
  • Focus, hover, disabled, and error appearances.
  • Layout changes caused by theme-specific assets or styles.

Decide whether each difference is an intended design change or a regression. Update the reference only after review. Playwright documents updating references with the --update-snapshots workflow in its visual comparisons guide.

6. Troubleshooting common problems

Symptom Likely cause What to do
The first run creates a screenshot instead of reporting a comparison. No reference exists for that test and screenshot name yet. Review the new image as the proposed baseline. Commit or otherwise preserve it with the test references so later runs can compare against it.
The same test passes locally but shows differences in another environment. Operating system, browser version or settings, hardware, or headless mode differs. Create and compare references in a consistent environment. Check the browser and host setup before treating the diff as an application defect.
The capture is light even though the test is meant to check dark mode. The scheme was not emulated for the context or page, or the app uses a separate toggle that was not activated. Set colorScheme: 'dark' or call page.emulateMedia({ colorScheme: 'dark' }). If the product uses a toggle, reproduce that interaction too.
Captures differ on every run. Content or page state is changing, for example timestamps, data, animation, or delayed loading. Use deterministic data, wait for the intended state, and manage known volatile regions with Playwright’s screenshot controls or custom screenshot styles.
A large diff appears after an intentional redesign. The saved reference describes the previous design. Review the changed areas and update the baseline only after confirming the new appearance is intended.
A small tolerance setting makes tests pass, but a visible dark-mode defect remains. The comparison is being made less sensitive than the defect warrants. Keep tolerance targeted to acceptable rendering variation. Investigate and fix meaningful differences such as unreadable text or missing boundaries.
A dark-mode change passes, but light mode is broken. Only the dark scheme was covered. Capture the same important states in both schemes with distinct reference names.

7. Performance, reliability, and cost

Screenshot comparisons are only useful when the page reaches a repeatable state and the reference environment remains stable. Cover high-value page templates and states first, then expand coverage where dark-mode-specific styling or user flows could introduce regressions. Avoid capturing redundant states that add maintenance without checking a distinct appearance.

Each test needs time to load and render its page, so many large pages or interaction states increase suite work. Keep navigation, data setup, and waits focused on the state under test. For reliability, make inputs deterministic, preserve reviewed references, and investigate environment changes when diffs appear across many unrelated pages.

Playwright is the software-based method described here; no physical product is needed. Project costs depend on the browser and CI setup you choose. The research sources provide no benchmark or pricing comparison for this workflow, so none is claimed.

Or skip the browser setup

ScreenshotNeo can return a website screenshot from one API request. It is a website screenshot API and MCP server for developers. Its clean-shot steps accept the cookie or consent banner like a visitor, then remove 60+ known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and responses identify the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf for AI agents and MCP clients. See the ScreenshotNeo site and API documentation.

For a visual check of a URL’s current appearance, make a call like this. It captures a page; it does not replace Playwright’s named baseline workflow or emulate dark mode.

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}`);
await Bun.write('shot.webp', res);

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; paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.

FAQ

Does dark-mode screenshot testing change a user’s display settings?

No. The test browser emulates the requested color scheme; it does not change the display setting on the developer’s computer or a user’s device.

Should I compare dark and light screenshots to each other?

Use separate references for each scheme. Compare each run with its matching scheme and page state so expected theme differences do not appear as regressions.

When is it safe to update a screenshot baseline?

After reviewing the diff and confirming that the changed appearance is intentional. A passing comparison against an unreviewed new reference cannot establish that the design is correct.