Recommended Free Tools
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.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →#1 Best Overall
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.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Crashes, No Sound, or Screen Glitches?
Random freezes, missing sound and display glitches usually trace back to one bad driver. Find and replace yours safely.Free scan · under a minuteViewport 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.
Rank #2
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.
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:
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 →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.
Rank #4
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.
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.
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.
Best Value
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_infoandcapture_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.
Which Puppeteer method captures a single component?
Use an element handle’s screenshot() method after selecting the element and waiting for it to exist.
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.




