To test a website’s dark mode with Puppeteer, emulate the prefers-color-scheme: dark media feature before taking a screenshot, then verify both the browser-exposed preference and the page’s rendered appearance. Capture a matching light-mode screenshot for comparison, keeping the browser, viewport, content, and readiness conditions the same.
Set up a repeatable dark- and light-mode capture
The example below uses Puppeteer’s documented API to open a page, emulate dark mode, check the preference exposed to the page, and save a screenshot. Replace the example URL with the page you want to test. Install Puppeteer in your project if it is not already installed.
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.setViewport({ width: 1440, height: 900 });
await page.goto('https://example.com', { waitUntil: 'networkidle2' });
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'dark' },
]);
const isDark = await page.evaluate(() =>
window.matchMedia('(prefers-color-scheme: dark)').matches
);
if (!isDark) throw new Error('Dark preference was not applied');
await page.screenshot({ path: 'dark.png' });
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'light' },
]);
await page.screenshot({ path: 'light.png' });
} finally {
await browser.close();
}
})();
Puppeteer documents page.emulateMediaFeatures() for emulating media features, including prefers-color-scheme. Its screenshot guide uses Page.screenshot() to capture a page. See the media emulation API and screenshot guide.
Wait for the page’s real readiness signal
waitUntil: 'networkidle2' is a useful navigation example, not proof that a page is visually settled. A site may render content after an API call, animate into place, lazy-load images, or keep network connections open. Prefer the readiness condition used by your application: for example, wait for a key selector, an explicit test hook, or a known loading indicator to disappear before capturing. Apply the same condition in both runs.
Recommended Free Tools
#1 Best Overall
Hold comparison conditions constant
For a useful light-versus-dark comparison, keep the URL, viewport dimensions, device scale factor, browser and Puppeteer versions, test data, and readiness criteria fixed. Save separate files so you can inspect both states side by side. If you change several variables at once, it becomes harder to tell whether a visual difference came from the theme or the test setup.
What the emulated preference means
The CSS media feature prefers-color-scheme lets a page detect whether the user has requested a light or dark scheme. Setting its value to dark tells the page that a dark preference is active. Setting it to light represents a light preference; in the CSS feature’s documented values, light also covers the absence of an active preference. See MDN’s prefers-color-scheme reference.
Rank #2
The check window.matchMedia('(prefers-color-scheme: dark)').matches confirms that the browser exposes the dark preference to page code. It does not confirm that the site’s CSS or components actually look correct in dark mode. The reverse check, window.matchMedia('(prefers-color-scheme: light)').matches, can help verify the light run.
Review the rendered screenshots
Inspect both images rather than treating a successful media-query check as a visual pass. Look for problems that can be hidden by testing only one theme or one part of the page:
- Text and background contrast, including secondary text.
- Links, buttons, borders, disabled states, and focus indicators.
- Logos, illustrations, charts, and other assets that may need theme-specific treatments.
- Overlays, dialogs, and theme-specific content that could affect layout.
- Native form controls and scrollbars, whose appearance can be influenced by the page’s color-scheme support.
For important behavior, pair visual review with explicit assertions in your test suite. A screenshot records one rendered state under the browser, viewport, content, and timing conditions you chose; it does not by itself establish broad browser compatibility or accessibility. Test the browser and viewport combinations your project supports.
Understand CSS color-scheme and browser-native UI
prefers-color-scheme and the CSS color-scheme property have related but different jobs. The media feature lets styles respond to the user’s preference. The color-scheme property tells the user agent which schemes an element can comfortably support, and can affect user-agent-provided UI such as form controls, canvas surfaces, and default scrollbar colors. Page components still need theme-aware styles. See MDN’s color-scheme reference.
Rank #4
A document that supports both schemes can declare <meta name="color-scheme" content="light dark"> in its head. The declaration communicates supported schemes and their preference order. MDN recommends placing it before styles so the user agent knows the preferred scheme early in rendering. It complements, rather than replaces, the site’s theme styling. See MDN’s color-scheme meta reference.
Capture a full page or a single component
For a long page, page.screenshot({ path: 'dark.png', fullPage: true }) captures the full page instead of only the current viewport. For a focused component comparison, Puppeteer also provides ElementHandle.screenshot(); locate the element and capture it directly. Choose the capture scope that matches the bug you are checking, and use the same scope for both schemes. See the Page.screenshot API and ElementHandle.screenshot API.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Repair Windows errors before they cause bigger problemsFix Now →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Troubleshoot common dark-mode screenshot problems
The screenshot still looks light
- Check the emulated value. Set
prefers-color-schemetodarkbefore capture and evaluate the darkmatchMediaquery. - Check whether the site responds to the preference. The browser may expose dark mode correctly while the site has no dark styles, uses a separate theme control, or applies a saved user setting.
- Check for app-controlled theme state. Some sites require a UI toggle or application-specific setup in addition to the system preference. Use the project’s own test setup for that state.
The screenshot catches a loading state or missing images
- Do not rely on network idle alone. Wait for a project-specific selector, loading-state change, or test hook that signals the content is ready.
- Make lazy content visible if needed. A viewport screenshot will not show content below the fold; use a full-page capture when the page’s lower sections are part of the test.
- Keep readiness logic identical. Different waits between the light and dark runs can produce differences unrelated to color scheme.
Native controls or scrollbars do not match the page theme
Check the document’s declared color-scheme support and the relevant browser behavior. The page’s dark CSS and the browser’s native UI are related but distinct; styling one does not guarantee that every user-agent control will follow it.
The API signature differs in the installed version
The cited Puppeteer API and guide pages label their documentation version 25.12.0, while the emulation reference is served from the /next/ documentation path. Check the version installed in your project and consult its corresponding API documentation if a method or signature does not match.
Or skip the browser setup
ScreenshotNeo offers a screenshot API and MCP server for developers. Its API takes a URL in one GET request; for a rendered dark-mode test, your application must respond to the browser’s dark preference. The call below captures the URL as WebP; see the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each of those steps can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response identifies the page verdict and billing status in headers. Its MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 shots per month with no card; paid plans start at $5 for 3,000 shots. Sign up for ScreenshotNeo’s free plan.
The Tool Desk
Outbyte Driver Updater FREEScan for outdated or missing drivers - takes under a minuteDriver Scan →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Frequently Asked Questions
Which Puppeteer version is this example for?
The cited API and guide pages label their documentation version 25.12.0; check the API reference matching the version installed in your project.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

