October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
dark mode

How to Take Screenshots in Dark Mode with Puppeteer

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

Use Puppeteer’s Page.emulateMediaFeatures() to set prefers-color-scheme to dark, then capture the page with Page.screenshot(). Set the preference before navigation so the document can read it from its first render. The complete pattern is:

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');
  await page.screenshot({ path: 'screenshot-dark.png', fullPage: true });
} finally {
  await browser.close();
}

This emulates the CSS media feature; it does not automatically operate a site’s custom theme switch, restore an application preference, or guarantee that every visual transition has finished. Those cases need page-specific setup and a deliberate readiness condition.

What the dark-mode setting actually changes

emulateMediaFeatures() changes the media features reported to the page. With prefers-color-scheme: dark, CSS such as @media (prefers-color-scheme: dark) can select its dark rules, and JavaScript using matchMedia('(prefers-color-scheme: dark)') can observe the dark preference. Puppeteer’s documented example verifies that the media query matches after emulation.

This is different from clicking a button labelled “Dark mode.” A web app may store a theme in local storage, a cookie, a server-side account setting, or component state and ignore prefers-color-scheme. In that situation, emulate the media feature and set the app’s own state as a separate step.

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

Install Puppeteer and create a reliable baseline

Prerequisites

  • A current Node.js installation and a project with Puppeteer installed.
  • A URL that the browser can reach from the machine running the script.
  • A writable location for the output image.

Install the package in your project with npm install puppeteer. Puppeteer launches a compatible browser for normal use; if your environment supplies its own browser, configure launch options according to that environment.

Minimal dark screenshot

Save this as an ES module (for example, dark-screenshot.mjs) and run it with node dark-screenshot.mjs:

import puppeteer from 'puppeteer';

const url = 'https://example.com';
const browser = await puppeteer.launch();

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

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

The try/finally closes Chromium even if navigation or capture throws. networkidle2 is a useful starting point for pages that make a finite set of requests, but it is not a universal “everything is visually ready” signal.

Choose the capture scope

The screenshot method can capture the viewport, the entire page, or a clipped rectangle. An element handle can capture one element after bringing it into view.

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

Viewport screenshot

await page.screenshot({
  path: 'dark-viewport.png',
  type: 'png',
});

Omit fullPage (or leave it false) when you need exactly the visible viewport. This is appropriate for visual regression at a fixed viewport or for a hero section that must not include content below the fold.

Full-page screenshot

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

fullPage: true captures content beyond the current viewport. Long, lazy-loaded pages can still require page-specific scrolling or a wait for images before capture; Puppeteer’s option alone does not establish that all application content has finished loading.

Clip a rectangle

await page.screenshot({
  path: 'dark-clip.png',
  clip: { x: 0, y: 0, width: 900, height: 600 },
});

The clip coordinates are page screenshot coordinates. Make sure the dimensions fit the layout you created with setViewport or the default viewport.

Capture one element

const card = await page.waitForSelector('.pricing-card');
if (!card) throw new Error('Pricing card was not found');
await card.screenshot({ path: 'pricing-card-dark.png' });

ElementHandle.screenshot() scrolls the element into view. It fails if the element has been detached from the DOM, so locate it after navigation and avoid replacing it between lookup and capture.

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

Control output format, quality and transparency

When you provide a path, Puppeteer can infer the image type from its extension. You can also set the type explicitly. PNG is lossless and supports transparency; JPEG is smaller for photographic content but does not preserve an alpha channel; WebP is useful when your downstream system accepts it.

await page.screenshot({ path: 'dark.webp', type: 'webp' });
await page.screenshot({ path: 'dark.jpg', type: 'jpeg', quality: 82 });
await page.screenshot({ path: 'dark-transparent.png', omitBackground: true });

The quality option applies to formats that support lossy quality settings. omitBackground: true requests a transparent page background; the result can still contain opaque backgrounds supplied by the document’s own CSS.

Make the page genuinely dark before capture

Set a viewport and device scale

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

Use a consistent viewport for repeatable captures. A higher device scale factor produces a denser image and can change responsive breakpoints only when the CSS viewport dimensions also change; choose it to match the consumer of the image.

Handle an application theme toggle

If the site has its own control, emulate the media feature first, then use the site’s documented UI or state mechanism. For a button, wait for it and click it before waiting for the resulting theme to settle:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
await page.emulateMediaFeatures([
  { name: 'prefers-color-scheme', value: 'dark' },
]);
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });

const toggle = await page.waitForSelector('[aria-label="Dark mode"]');
if (toggle) {
  await toggle.click();
}
await page.waitForTimeout(300);
await page.screenshot({ path: 'app-dark.png', fullPage: true });

The selector and delay are examples, not universal requirements. Prefer a deterministic signal from the application—such as a dark-theme class, an ARIA state, or a completed transition—over an arbitrary delay when one is available.

Wait for fonts, images and dynamic content

Navigation readiness and visual readiness are separate concerns. Add only the waits your page needs:

await page.goto(url, { waitUntil: 'networkidle2' });
await page.evaluate(() => document.fonts.ready);
await page.waitForSelector('main[data-ready="true"]');
await page.screenshot({ path: 'ready-dark.png', fullPage: true });

If images are lazy-loaded, scroll in controlled increments or use the page’s own “loaded” signal before taking a full-page shot. If animations create nondeterministic frames, disable them with CSS for the capture or wait until the relevant transition ends. Do not assume one universal wait covers arbitrary fonts, images, animations and theme transitions.

Debug whether dark mode reached the page

Check the media query from inside the page:

const isDark = await page.evaluate(() =>
  window.matchMedia('(prefers-color-scheme: dark)').matches
);
console.log({ isDark });

true confirms the emulated media feature is visible to page JavaScript. It does not prove that the page has dark CSS, that a framework has applied its theme class, or that a custom toggle has changed state. Inspect the computed styles or the application’s theme marker when diagnosing those cases.

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

Common failures and fixes

The screenshot is still light

  • Cause: the site uses a custom theme preference rather than the media query.
  • Fix: set the site’s cookie, local-storage value, account preference, or toggle in addition to calling emulateMediaFeatures().

The dark preference is not detected

  • Cause: emulation was applied after navigation or the page was replaced.
  • Fix: create the page, emulate the feature, then navigate. Reapply it if your code creates a new page or reloads into a different context.

Missing content in a full-page image

  • Cause: lazy loading, virtualization, or an application request still in progress.
  • Fix: trigger the page’s loading path, wait for a meaningful selector or application-ready signal, and then capture.

Node is detached from document

  • Cause: a framework rerendered the element after you obtained its handle.
  • Fix: wait for the rerender to finish and obtain a fresh handle immediately before elementHandle.screenshot().

Navigation timeout or blocked page

  • Cause: slow resources, a network restriction, authentication, or a bot check.
  • Fix: verify the URL from the capture host, provide the required authentication in the page context, and choose a timeout appropriate to the page. Do not treat a bot challenge as a successful screenshot.

Animations make captures differ

  • Cause: the frame was captured at a different point in an animation.
  • Fix: disable motion with page-specific CSS or wait for a stable state before capture.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Performance, reliability and cost considerations

Launching a browser for every image is simple but expensive in time and memory. For a batch, keep one browser process and create isolated pages or browser contexts, while closing each page after capture. Limit concurrency to what the machine can sustain; too many simultaneous pages increase contention and make timeouts more likely.

Use a fixed viewport, deterministic test data, stable waits and explicit output names for reproducible artifacts. Record the URL, viewport, emulated media feature, readiness condition and capture time with each artifact. For authenticated pages, keep credentials out of source control and clear the context when the job ends.

Puppeteer itself does not charge per screenshot; your costs are the compute, storage, bandwidth and any hosted-browser service you add. A self-managed browser also leaves you responsible for browser updates, sandbox configuration, retries and observability.

Or skip the browser setup

ScreenshotNeo provides a website screenshot API and MCP server. Its dark-mode option can capture a URL without maintaining Puppeteer infrastructure, and its broader capture controls include full-page shots, element selectors, device presets, retina scale, custom CSS and JavaScript, waits, cookies, headers and PDF output.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

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

One GET request returns an image or PDF. See the ScreenshotNeo documentation for the complete parameter list.

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}`);
  • Cookie banners, newsletter popups and chat widgets are removed before the shot; each cleanup step can be turned off.
  • Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed as clean shots. Response headers report the page verdict and billing status.
  • An MCP server lets Claude, Cursor and other MCP clients use take_screenshot, get_page_info and capture_pdf.
  • The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is on every plan.

Sign up for the free ScreenshotNeo plan to try it without a card.

Frequently Asked Questions

Can Puppeteer emulate dark mode after the page has loaded?

Yes, but setting the media feature before navigation gives the page the preference from its initial render and avoids missing early theme decisions.

Does dark-mode emulation change screenshots of every website?

No. It changes the reported CSS media feature. Sites that rely on a separate toggle, cookie, local-storage value or account setting need that state changed too.

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

Which Puppeteer method captures a single component?

Use an element handle’s screenshot() method after selecting the element and waiting for it to exist.

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.

Leave a Reply

Your email address will not be published. Required fields are marked *

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

Read next

Recommended PC Tool
Recommended PC Tool
Windows Errors? Fix Them Before They SpreadFree repair scan
Outdated Drivers Are Slowing You DownFree scan - exact matches

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.