Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

To take a dark-mode screenshot with Puppeteer, emulate the CSS media feature prefers-color-scheme: dark with page.emulateMediaFeatures(), then capture the page with page.screenshot(). Set the emulation before navigating so the page can see the preference as it loads. This works when the site uses that media feature; sites with their own theme toggle or saved theme setting may need an additional, site-specific step.

Set up Puppeteer and capture a page in dark mode

The example below uses Puppeteer’s JavaScript API and saves a full-page PNG. It assumes Node.js is installed and that the current directory can install npm packages.

  1. Create a project if you do not already have one: npm init -y.

  2. Install Puppeteer: npm install puppeteer.

  3. Save the following as screenshot-dark.mjs, replacing the example address with the page you want to capture.

    Free tools Windows power users keep installed

    One-click scans. No signup required.

    Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  4. Run it with node screenshot-dark.mjs. The output file will be screenshot-dark.png in the current directory.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();

  // Emulate dark preference before navigation.
  await page.emulateMediaFeatures([
    { name: 'prefers-color-scheme', value: 'dark' },
  ]);

  await page.goto('https://example.com', {
    waitUntil: 'domcontentloaded',
    timeout: 30_000,
  });

  // Optional diagnostic: confirm the CSS media query evaluates to true.
  const prefersDark = await page.evaluate(
    () => window.matchMedia('(prefers-color-scheme: dark)').matches,
  );
  console.log('prefers-color-scheme: dark:', prefersDark);

  await page.screenshot({
    path: 'screenshot-dark.png',
    fullPage: true,
  });
} finally {
  await browser.close();
}

The diagnostic should print true if the browser is emulating the preference. It verifies the media feature, not that the website actually applied a dark theme: the site’s styles and application logic determine how that preference changes the page.

What dark-mode emulation changes—and what it does not

emulateMediaFeatures() sets a browser media feature. It does not click a theme button, change an account preference, or set a site’s local storage or cookie. A page using CSS such as @media (prefers-color-scheme: dark) can respond to the emulated preference. A page using only its own theme state may remain light.

For a site with an explicit theme control, determine how that specific application stores and applies the choice, then set it before capturing. Depending on the site, that could mean interacting with a visible toggle after navigation or supplying a documented preference cookie or storage value. Do not assume a generic storage key: names and behavior are site-specific, and changing the wrong value can have no effect or alter unrelated state.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

If the theme is set by a control that appears only after the page loads, navigate first, wait for the control, and interact with it before taking the screenshot. If the site offers both a system theme and a manual override, a manual selection may take precedence over prefers-color-scheme. For debugging, inspect the page after applying the preference and check whether its styles, class names, or theme state changed—not just the value returned by matchMedia().

Choose the right capture area and output

Puppeteer’s screenshot method captures the page. Set the capture scope according to what the result must contain rather than treating full-page capture as a substitute for choosing a useful viewport.

Need Approach Important detail
What is visible in the viewport page.screenshot() without fullPage Choose the viewport dimensions before navigation or capture if the layout must match a particular screen size.
The full scrollable page page.screenshot({ fullPage: true }) Long pages can produce very tall images. Lazy-loaded content may need scrolling or another page-specific readiness step before capture.
A defined rectangular region Use the screenshot clip option The rectangle must correspond to the page area you intend to capture; verify its coordinates and dimensions against the chosen viewport.
One element Find the element and call ElementHandle.screenshot() Puppeteer scrolls the element into view if needed. The call throws if the element has been detached from the DOM.

For example, capture a specific element after waiting for it to appear:

const card = await page.waitForSelector('.product-card', { timeout: 10_000 });
if (!card) throw new Error('Product card was not found');
await card.screenshot({ path: 'product-card-dark.png' });

Screenshot options include a file path, image type, quality for applicable formats, a clip region, and omitBackground for transparency. When a path is supplied, the filename extension can determine the image type. For example, use a matching extension such as .png or .jpg rather than relying on an extension that disagrees with the intended output. Quality applies to formats that support it; it is not a way to improve PNG quality.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Wait for the page state you actually need

waitUntil: 'domcontentloaded' in the example waits for the document’s initial HTML parsing to finish. It does not guarantee that a client-rendered application, remote fonts, images, animations, or a theme transition has completed. There is no single readiness wait that guarantees every website is visually ready.

  • Wait for a meaningful element: use page.waitForSelector() for a heading, chart, or other target that indicates the relevant content has appeared.
  • Wait for application state: if the site exposes a stable, page-specific condition, use page.waitForFunction() to wait for it.
  • Allow a known transition to finish: if changing the theme triggers an animation, wait for the site’s state or transition to settle before capture. A fixed delay can be useful as a last resort, but may be too short on a slow run and unnecessarily long on a fast one.
  • Handle lazy content deliberately: a full-page screenshot does not establish that every off-screen image has loaded. Scroll through the page or wait for the relevant images if those assets matter to the result.

Network-idle waits can help with some pages, but they are not a universal visual-readiness test. Pages that keep connections open or continue background requests may not become idle in the way a capture workflow expects. Prefer a condition tied to the content you need whenever possible.

Troubleshoot common dark-mode capture problems

The screenshot is still light

  • Confirm the emulation call runs before navigation and uses the exact feature name prefers-color-scheme with value dark.
  • Check the diagnostic result from matchMedia(). If it is false, the preference was not applied to the page as expected.
  • If it is true, the browser preference is active but the page may not use it. Look for a site theme toggle, a saved manual choice, or custom application state and set that state separately.
  • Wait for the application to render or finish a theme transition before capturing. A screenshot taken immediately after changing a control can show an intermediate state.

Navigation times out or the browser does not launch

  • A navigation timeout means the selected navigation condition was not reached before the timeout. Check that the URL is reachable from the machine running Puppeteer, then choose a suitable readiness condition and wait for the specific content needed.
  • If the page loads but continues background network activity, avoid treating network idle as a requirement unless that behavior suits the site. Wait for the target element instead.
  • If Chromium fails to launch, check the installation output and the runtime environment for browser dependencies or execution restrictions. The screenshot call cannot run until Puppeteer has a working browser process.

The full-page image is incomplete, blank, or unexpectedly large

  • Wait for the page’s actual content, not only document parsing. For lazy-loaded sections, make the page reveal those sections and confirm their assets are ready.
  • Use an element capture or a clip if only part of a very long page is needed. This also makes the output dimensions more predictable.
  • Check whether the target element was replaced during rendering. An element handle becomes unusable if the element is detached; locate it again after the page updates.
  • Review viewport size and capture options if the layout differs from the expected design. Responsive pages can render different content at different viewport widths.

The output format or transparency is wrong

  • Make the filename extension and requested image type consistent. If the path supplies the format by its extension, a misleading extension can produce confusing results.
  • Use omitBackground when you need a transparent page background, and verify that the chosen image format preserves transparency.
  • Do not rely on a quality option for formats where quality is not applicable; use the format’s supported output behavior instead.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability, and cost considerations

Each Puppeteer capture launches or uses a browser and loads the target page, so capture time depends on the browser environment, page weight, network, and the readiness condition. Reusing a browser for multiple captures can avoid repeated browser startup, but isolate page state carefully when requests have different cookies, authentication, or theme settings. Always close the browser in cleanup logic so errors during navigation or capture do not leave browser processes running.

For repeatable images, fix the viewport, use the same theme setup, and wait for a meaningful page condition. Dynamic content, personalized state, changing network assets, and animation can make two captures differ even if the screenshot code is unchanged. A timeout is a limit on waiting, not proof that the page is visually complete.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Puppeteer itself does not establish a per-screenshot service price in this workflow. Your practical costs are the infrastructure and runtime used to run the browser, plus any costs associated with the target site or hosting environment. If you need an API rather than operating the browser, see the alternative below.

Or skip the browser setup

ScreenshotNeo is a website screenshot API with a dark-mode option and an MCP server for AI agents. Its clean-shot workflow accepts consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status.

The following is the supplied one-call cURL capture example. It downloads a WebP screenshot; consult the ScreenshotNeo API documentation for the dark-mode option and current request parameters when you need the captured page rendered in dark mode.

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

ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month with no card.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.