ScreenshotNeo

BlogHow-to

How to Test Dark Mode with Playwright Screenshot Comparisons

Emulate light and dark color schemes in Playwright, compare screenshots against reviewed baselines, and troubleshoot visual diffs reliably.

By the ScreenshotNeo team4 October 20268 min read

Use Playwright’s colorScheme emulation to make the page believe the user prefers dark mode, then use Playwright Test’s expect(page).toHaveScreenshot() to compare the rendered page with a checked-in reference image. Test light and dark as separate states, keep the browser and capture environment consistent, and review visual diffs before accepting baseline changes.

1. Set up a dark-mode screenshot test

The examples use Playwright Test. Install it and initialize the project if you have not already:

npm init playwright@latest

Choose the test language and browser configuration appropriate to your project during setup. Create a test such as tests/dark-mode.spec.ts:

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

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

Configure baseURL in Playwright Test so page.goto('/') points to your application. For example, add this to playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  use: {
    baseURL: 'http://127.0.0.1:3000',
  },
});

Start the application using your normal development or preview command before running the test, or configure Playwright Test’s web server option to start it. The first screenshot assertion creates a reference image. Later runs compare the new image with that baseline. Run tests with:

npx playwright test tests/dark-mode.spec.ts

Screenshot assertions are part of the Playwright Test runner, so this pattern uses @playwright/test, not just a browser launched through the Playwright library. See the official visual comparisons guide and page assertion reference.

2. Choose how to emulate the color scheme

Playwright documents three useful places to set the emulated scheme. Use the one that matches the scope of the test:

Scope Example Good fit
Project use: { colorScheme: 'dark' } A project dedicated to dark-mode coverage
Test test.use({ colorScheme: 'dark' }) A group of tests that all exercise dark mode
Page during a test await page.emulateMedia({ colorScheme: 'dark' }) A test that needs to switch modes or compare both states
Browser context browser.newContext({ colorScheme: 'dark' }) Tests using the Playwright library directly or creating custom contexts

For a dark-only project, add the setting to a project in playwright.config.ts:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    {
      name: 'chromium-dark',
      use: {
        browserName: 'chromium',
        colorScheme: 'dark',
      },
    },
  ],
});

For a small test group, scope it with test.use:

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

test.describe('dark mode', () => {
  test.use({ colorScheme: 'dark' });

  test('shows the dark theme', async ({ page }) => {
    await page.goto('/');
    await expect(page).toHaveScreenshot('home-dark.png');
  });
});

For a test that switches modes, call page.emulateMedia before each capture. Playwright documents light and dark as supported color schemes. Check the installed Playwright version’s API reference if your project uses a version different from the current documentation.

3. Test dark and light mode independently

If your product supports both system themes, save a separate snapshot for each. Explicit snapshot names make the expected state clear during review.

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

test('renders the home page in light and dark modes', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'light' });
  await page.goto('/');
  await expect(page).toHaveScreenshot('home-light.png');

  await page.emulateMedia({ colorScheme: 'dark' });
  await expect(page).toHaveScreenshot('home-dark.png');
});

For larger suites, separate projects or test groups can make it easier to identify which state failed. A project-per-scheme configuration might look like this:

import { defineConfig } from '@playwright/test';

export default defineConfig({
  projects: [
    { name: 'chromium-light', use: { browserName: 'chromium', colorScheme: 'light' } },
    { name: 'chromium-dark', use: { browserName: 'chromium', colorScheme: 'dark' } },
  ],
});

Use a deliberate browser or operating-system matrix when cross-browser rendering is part of the requirement. More projects mean more captures and more baselines to maintain; add them for a defined coverage need rather than assuming one screenshot is portable across every environment.

4. Make screenshot captures repeatable

Visual assertions are useful only when a real design change can be distinguished from a change in the capture environment. Playwright’s visual comparison documentation cautions that rendering can vary with the host operating system, browser version, settings, hardware, power source, headless mode, and other factors. Run baseline generation and comparison with consistent browser versions, operating systems, fonts, viewport, and capture settings. Keep separate baselines when those environments are intentionally different. See Playwright’s guidance on visual comparisons.

Wait for the intended page state

Navigate to the correct route, wait for application data and fonts that affect layout, and put the page in the state under test before capturing. Prefer a meaningful condition over a fixed delay when the app exposes one:

await page.goto('/settings');
await page.getByRole('heading', { name: 'Settings' }).waitFor();
await expect(page).toHaveScreenshot('settings-dark.png');

toHaveScreenshot() waits until two consecutive screenshots match before comparing the final capture with the baseline. This helps with transient rendering, but does not make different browsers or hosts equivalent, nor does it ensure that the page reached the correct application state. See the snapshot assertion documentation.

Suppress only irrelevant motion or dynamic content

The assertion’s stylePath option can apply a stylesheet while capturing. It can hide content such as a clock or rotating ad if that content is genuinely outside the test’s purpose. Keep any masking narrow: hiding a theme-aware widget or color that should change in dark mode can conceal the very regression the test should detect.

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

test('captures a stable dark-mode page', async ({ page }) => {
  await page.emulateMedia({ colorScheme: 'dark' });
  await page.goto('/');
  await expect(page).toHaveScreenshot('home-dark.png', {
    stylePath: './tests/screenshot-stability.css',
  });
});
/* tests/screenshot-stability.css */
/* Hide only a known, irrelevant source of visual variation. */
.test-clock,
.rotating-ad {
  visibility: hidden !important;
}

Check the option against the installed version’s page assertion reference; configuration options can vary by version.

5. Set comparison tolerances deliberately

Start with strict comparisons. If small rendering variation produces noisy diffs in a stable environment, Playwright offers ways to allow limited differences:

  • threshold sets the acceptable perceived color difference for a pixel. The API reference documents a YIQ color-difference threshold with a default of 0.2.
  • maxDiffPixels caps the number of pixels that may differ.
  • maxDiffPixelRatio caps the proportion of differing pixels.

For example, a team might set a small pixel budget after inspecting the diff and identifying harmless rendering variation:

await expect(page).toHaveScreenshot('home-dark.png', {
  threshold: 0.2,
  maxDiffPixels: 100,
});

The values above illustrate documented options; they are not universal recommendations. A larger threshold or pixel budget can make noise less disruptive, but it can also let a real contrast, border, icon, or layout regression pass. Review the diff and tune only to the visual contract you intend to accept. See the snapshot assertion API reference.

6. Review and update baselines

When a test fails, inspect the expected image, actual image, and diff before changing the reference. If the design change is intentional, update snapshots with:

npx playwright test --update-snapshots

Review the generated image changes, then commit approved baselines with the test code. Playwright recommends keeping snapshots in version control and reviewing their changes. Do not update snapshots automatically just to turn a failing build green; that can make an unintended visual regression the new expectation. Read Playwright’s baseline update guidance.

7. Common problems and fixes

Symptom Likely cause Fix
The page stays light after setting dark mode The app does not respond to prefers-color-scheme, or its theme is controlled by an app setting instead. Confirm the application supports the media feature. If the app uses a theme toggle or stored preference, set that state in the test as well as emulating the system scheme.
The first run fails because no baseline exists The test runner is creating its initial expected screenshot. Run the test, inspect the created image, and commit it if it represents the intended appearance.
Screenshots differ across machines Browser, operating system, fonts, hardware, settings, or headless rendering differ. Generate and compare in a consistent environment, or keep environment-specific references for intentional platform coverage.
Intermittent diffs on an otherwise stable page Content is still changing, such as a clock, animation, delayed image, or rotating promotion. Wait for the relevant page state, stabilize the data, and use a narrowly scoped stylePath only for irrelevant dynamic elements.
A dark-mode defect passes unexpectedly The threshold or differing-pixel allowance may be too permissive, or a stylesheet may hide the changed area. Inspect assertion options and masking styles; reduce tolerance or remove masking around behavior under test.
Test reports that toHaveScreenshot is unavailable The test is not using Playwright Test’s assertion API, or the imported package/version differs. Use expect from @playwright/test and consult the API docs for the installed version.
Baseline update changes many unrelated files The capture environment or broad styling changed, or the update command refreshed more tests than intended. Review the diff, narrow the test selection for updates, and verify browser and environment consistency before accepting snapshots.

8. Performance, reliability, and maintenance

  • Keep the matrix useful. Each browser, scheme, and platform combination adds work and reference images. Cover combinations your users or product requirements need.
  • Use stable state setup. Reuse deterministic test data and wait for the page condition relevant to the screenshot; avoid arbitrary sleeps where an observable condition is available.
  • Separate environment variation from product changes. Pin or otherwise standardize the test browser and execution environment where practical. Keep distinct snapshots for intentionally different platforms.
  • Make snapshot changes reviewable. Include visual diffs in code review and commit approved references alongside the test changes.
  • Spend tolerance carefully. Begin with a strict assertion, inspect noisy diffs, and make any difference budget small and justified by the rendering contract.
  • Budget CI work by coverage. Running extra projects and capturing large pages costs additional execution time and storage. No universal runtime or cost figure applies; measure it in your own CI environment.

Or skip the browser setup

If you need a clean capture without building a Playwright visual-test harness, ScreenshotNeo is a website screenshot API and MCP server for developers. It accepts a URL in one GET request and returns PNG, JPEG, WebP, or PDF. Use its documented API parameters and your own target page; for example, this request captures a URL as WebP:

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

See the ScreenshotNeo API documentation for request options, including dark mode. 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 screenshots. For Playwright baseline assertions and controlled test environments, use the workflow above; an API screenshot is a convenient capture path, not a replacement for your test runner’s reviewed visual baselines. Sign up free for 1,000 screenshots a month, with no card required.

Frequently asked questions

How to test dark mode with Playwright?

Emulate colorScheme: 'dark' on the project, test, context, or page, then capture a named snapshot with Playwright Test’s toHaveScreenshot().

How do I compare screenshots in Playwright?

Use expect(page).toHaveScreenshot('name.png') in a Playwright Test test. The initial run creates a baseline; future runs compare against it. Review diffs before approving baseline updates.

Does emulating dark mode change my application’s saved theme setting?

No. Color-scheme emulation sets the browser media preference. If your application uses a separate theme toggle or persisted setting, set that application state explicitly in the test too.

Should I use one baseline for every browser?

Use one only when the captures come from the same intended rendering environment. If browser or platform differences are part of your coverage, maintain and review the corresponding expectations separately.