ScreenshotNeo

BlogHow-to

Capture a Website Screenshot with a Dark Color Scheme Using Puppeteer

Use Puppeteer to emulate prefers-color-scheme: dark, wait for the page to render, and save a viewport or full-page screenshot.

By the ScreenshotNeo team4 October 20266 min read

Use Puppeteer’s page.emulateMediaFeatures() to set the browser’s prefers-color-scheme preference to dark, then capture the rendered page with page.screenshot(). Set the preference before navigation so the site can use it during startup. This asks the site to render its dark theme; it cannot make a site dark if the site does not support that preference.

import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
try {
  const page = await browser.newPage();
  await page.emulateMediaFeatures([
    { name: 'prefers-color-scheme', value: 'dark' },
  ]);

  await page.goto('https://example.com', { waitUntil: 'networkidle2' });
  await page.screenshot({ path: 'screenshot.png', fullPage: true });
} finally {
  await browser.close();
}

The script uses JavaScript modules. Install Puppeteer with npm install puppeteer, save the code as screenshot.mjs, and run node screenshot.mjs. Puppeteer’s media emulation API documents the dark preference, and its screenshot guide documents Page.screenshot().

1. Set the dark preference and capture

  1. Launch Puppeteer and create a page.
  2. Call page.emulateMediaFeatures([{ name: 'prefers-color-scheme', value: 'dark' }]).
  3. Navigate to the target URL and wait for the page state your capture needs.
  4. Call page.screenshot() with a path and any relevant capture options.
  5. Close the browser in a finally block so it is cleaned up after navigation or capture errors.

To confirm that the browser preference is active, evaluate the media query:

const isDarkPreference = await page.evaluate(() =>
  matchMedia('(prefers-color-scheme: dark)').matches
);
console.log(isDarkPreference); // true

This confirms the emulated preference, not that the website has implemented a dark theme. The site may ignore the preference, force a theme through its own setting, or apply styles after additional client-side work.

2. Choose viewport or full-page capture

By default, the screenshot captures the visible viewport. Use fullPage: true to capture the full page height:

await page.screenshot({ path: 'full-page.png', fullPage: true });

For a viewport screenshot, omit fullPage or set it to false. Set the viewport before navigation if the page’s responsive layout depends on its dimensions:

await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 });

For example, use a viewport capture for a page preview or a specific responsive breakpoint; use full-page capture when the whole document is needed. Puppeteer’s ScreenshotOptions reference covers screenshot options and output formats. Match the file extension to the format you request.

3. Wait for the right page state

waitUntil: 'networkidle2' is a useful starting point, but it is not a universal signal that every page is visually ready. A site may keep network connections open, load images lazily as you scroll, animate elements, or render important content after a client-side event.

When you know a page-specific readiness condition, wait for it explicitly. For example, if the main content has a stable selector:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.waitForSelector('main article');
await page.screenshot({ path: 'article.png', fullPage: true });

You can also use a short delay for a known animation or delayed render, but prefer a selector or application-specific readiness check when one is available. No single wait setting guarantees that every image, font, animation, and asynchronous widget has finished.

4. Run it from the command line with cURL

cURL does not emulate a browser’s color-scheme preference or render a web page, so it cannot produce a Puppeteer screenshot by itself. Use it to run a local Puppeteer script through an HTTP service only if you have built or deployed such a service; do not treat a normal URL request as equivalent to browser capture. For a managed screenshot API, use the ScreenshotNeo request below.

5. Python alternative with browser automation

The requested implementation is Puppeteer, a JavaScript library. Python’s requests library cannot emulate browser media features or render a page. If your Python application needs the same browser behavior, use a browser automation library that supports media emulation, or call a screenshot API. ScreenshotNeo’s Python request is shown below.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. Its API returns a screenshot or PDF from one GET request. The API accepts screenshot parameters used by other screenshot APIs, and its documentation explains available options. For a dark scheme, pass the API’s dark-mode option as documented.

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 import('node:fs/promises').then(({ writeFile }) => writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before capture; bot checks, blank pages, failed loads, and cache hits are never billed. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. 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 required.

Troubleshooting

Symptom Likely cause Fix
The screenshot is still light The site does not use prefers-color-scheme, overrides it with a stored or explicit theme setting, or has not finished applying its styles. Check matchMedia('(prefers-color-scheme: dark)').matches. If it is true, inspect the site’s theme behavior and wait for its relevant render state.
The media query returns false The emulation was not applied to the page you are checking, or the feature name/value was entered incorrectly. Call emulateMediaFeatures on the same page before navigation, using the exact feature name prefers-color-scheme and value dark.
The screenshot is blank or missing content Capture occurred before client-side content or a required selector appeared. Wait for a stable page-specific selector or readiness condition before calling screenshot().
Images are missing in full-page output Images may load lazily only when their part of the page approaches the viewport. Scroll through the page or use a site-specific image readiness strategy before capturing; verify the resulting image.
Navigation hangs or times out The page may keep connections open, making a network-idle condition unsuitable. Try domcontentloaded followed by a selector wait. Set an appropriate navigation timeout for the target and handle timeout errors.
The process remains open after an error The browser did not close when navigation or capture threw. Put browser cleanup in finally, as in the runnable example.

Performance, reliability, and cost

For a single capture, reusing one browser process for multiple pages can avoid repeated launch overhead; close each page when finished and close the browser when the job ends. Full-page captures and high device scale factors produce larger images and can take more memory and time than viewport captures. Limit concurrency to what the host can support, and set navigation and selector timeouts appropriate to the target site.

Capture reliability depends on the site’s load behavior. A network-idle event alone may be early for lazy content or never arrive for pages with persistent requests. Prefer explicit readiness checks and record navigation or screenshot failures so jobs can be retried selectively. Puppeteer is software you run and operate; its direct monetary cost depends on your browser host and infrastructure.

With ScreenshotNeo, only clean shots are billed, and each response reports the page verdict and billing state in X-Page-Verdict and X-Billed headers. Plans are Free for 1,000 shots a month, 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. Every feature is on every plan. See the API documentation for configuration details.

FAQ

Does setting the preference force every site into dark mode?

No. It sets the browser’s media preference. The site must implement a dark scheme and honor that preference.

Should I set dark mode before or after navigation?

Before navigation is the safer sequence when the site chooses its initial theme during startup.

Does fullPage: true scroll the page like a person?

It requests a full-document screenshot. It does not guarantee that lazy content has loaded; handle page readiness separately.

Can I verify the screenshot is dark by checking the media query?

The check verifies the browser preference only. Inspect the captured output to confirm the site rendered dark styles.