ScreenshotNeo

BlogHow-to

How to Capture a Website Screenshot in Dark Mode with an API

Use Playwright to emulate a dark color-scheme preference before capture. Learn what that changes, how to save a screenshot, and when a screenshot API can simplify the workflow.

By the ScreenshotNeo team4 October 20267 min read

To capture a website screenshot in dark mode, ask the browser to emulate the prefers-color-scheme: dark media preference before taking the screenshot. With Playwright, set colorScheme: 'dark' when creating the browser context, or call page.emulateMedia({ colorScheme: 'dark' }) on an existing page. The website must respond to that preference for its own dark styling to appear.

This guide uses Playwright with Node.js for the do-it-yourself browser workflow, then shows the alternative of using ScreenshotNeo, a hosted website screenshot API. Playwright supports light and dark color schemes in its browser context and media emulation APIs. See the Browser API and Page API.

1. Install Playwright

Use a current Node.js installation. In a new project directory, install Playwright and its Chromium browser:

npm init -y
npm install playwright
npx playwright install chromium

Save the following script as dark-screenshot.js. It opens a page with a dark color-scheme preference, checks whether the page can see that preference, and saves a full-page PNG.

2. Capture the page with dark preference enabled

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const context = await browser.newContext({
      colorScheme: 'dark',
      viewport: { width: 1440, height: 900 },
    });
    const page = await context.newPage();

    await page.goto('https://example.com', { waitUntil: 'load' });

    const seesDarkPreference = await page.evaluate(() =>
      window.matchMedia('(prefers-color-scheme: dark)').matches
    );
    console.log('Page sees dark preference:', seesDarkPreference);

    await page.screenshot({ path: 'screenshot.png', fullPage: true });
    await context.close();
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Run it with:

node dark-screenshot.js

The matchMedia check confirms that the browser exposes the dark preference to the page. It does not prove that the site has dark styles or that every component has switched themes.

3. Set dark mode on an existing page

If you need to create the page first or change its color preference later, use page.emulateMedia(). Set the preference before the final capture so the page has an opportunity to update.

const { chromium } = require('playwright');

(async () => {
  const browser = await chromium.launch();
  try {
    const page = await browser.newPage();
    await page.goto('https://example.com', { waitUntil: 'load' });

    await page.emulateMedia({ colorScheme: 'dark' });
    await page.screenshot({ path: 'screenshot-dark.png', fullPage: true });
  } finally {
    await browser.close();
  }
})().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

For a site that reads the preference during its initial render, prefer setting colorScheme on the browser context before navigation. That way the preference is present from the start.

4. Choose the capture scope and page state

Dark preference and screenshot scope are separate decisions. Playwright’s page.screenshot() captures the page; passing fullPage: true requests the full scrollable page rather than just the current viewport. For a viewport capture, omit fullPage or set it to false.

Need Playwright choice Consideration
Dark styling from the start browser.newContext({ colorScheme: 'dark' }) Configure before navigation.
Change preference on an existing page page.emulateMedia({ colorScheme: 'dark' }) Allow the site to update before capture.
Current viewport only page.screenshot({ path: 'shot.png' }) Uses the current viewport dimensions.
Whole scrollable page page.screenshot({ path: 'shot.png', fullPage: true }) Very long pages can produce large images.

Set viewport dimensions in the context when a particular layout matters. For example, a site may use a different navigation layout at 390 pixels wide than at 1440 pixels. The screenshot format and other capture options are documented in the Playwright screenshot API reference.

Choose the navigation readiness condition for the page you are capturing. The example uses waitUntil: 'load'; that event does not guarantee that client-side rendering, images, or later network requests are finished. If the page has a known element that appears when it is ready, wait for that element before capturing:

await page.goto('https://example.com', { waitUntil: 'load' });
await page.locator('main').waitFor({ state: 'visible' });
await page.screenshot({ path: 'ready.png', fullPage: true });

Replace main with a selector that represents readiness on your target site. There is no single wait condition that fits every application.

5. Confirm the site actually supports dark mode

Dark emulation changes the browser preference exposed through prefers-color-scheme. It does not rewrite a site’s CSS, click its theme toggle, or guarantee that the site has a dark theme. A site may remain light if it ignores the preference, uses a saved user setting, or requires an in-page control.

If the target has a theme switcher, first emulate dark mode and then interact with that control when needed. If the theme depends on a logged-in account or stored preference, set up that state explicitly in the browser context. Keep in mind that a screenshot represents the state reached by your script; inspect the relevant page elements or colors if visual correctness matters.

6. Use cURL, Python, or Node.js with a hosted screenshot API

A browser library gives you control of navigation and page interaction. A hosted API is useful when you want to submit a URL without managing the browser process and its installed browser binaries. ScreenshotNeo is a website screenshot API and MCP server for developers. Its API supports dark mode, and its documentation describes the request options. Configure the dark mode option there for the capture; the basic URL request below demonstrates saving the returned image.

cURL

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

Python

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as f:
    f.write(r.content)

Node.js

const q = new URLSearchParams({
  access_key: 'YOUR_API_KEY',
  url: 'https://example.com',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
require('node:fs').writeFileSync('shot.webp', image);

The examples use the API base https://api.screenshotneo.com/v1/shot. Add the documented dark-mode option to the request as shown in the ScreenshotNeo API docs. Keep your access key on the server side; do not expose it in public browser code. ScreenshotNeo also supports custom viewport and device presets, full-page capture, waiting for a selector or network idle, custom CSS and JavaScript, cookies, headers, and other capture settings.

Or skip the browser setup

ScreenshotNeo accepts a URL in one GET request and returns an image or PDF. Its dark mode option is documented in the API docs. For example, this cURL request saves a screenshot of Stripe:

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

ScreenshotNeo removes cookie banners, popups, and chat widgets before the shot; bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Learn about ScreenshotNeo, then sign up free.

Performance, reliability, and cost

  • Browser setup: Reuse a browser process for multiple captures when running a batch, while creating a fresh context when you need isolated cookies and settings. Close pages, contexts, and the browser when finished.
  • Wait deliberately: Waiting for a specific visible element can avoid both premature captures and unnecessary fixed delays. A network-idle condition may not be suitable for sites that continuously poll.
  • Full-page output: Full-page captures can take longer and produce larger files than viewport captures. Use viewport capture if the task does not need content below the fold.
  • Hosted API cost: ScreenshotNeo bills only clean shots. Its response identifies page verdict and billing status with X-Page-Verdict and X-Billed headers; cache hits cost nothing. Plans are Free: 1,000/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.
  • Reliability: For repeatable screenshots, control the URL, viewport, color preference, page readiness condition, and any site-specific interaction. Dynamic content may still change between captures.

Troubleshooting

Symptom Likely cause Fix
The screenshot stays light The site does not implement dark styles for prefers-color-scheme, or another theme setting overrides it. Check matchMedia('(prefers-color-scheme: dark)').matches. If it is true, use the site’s theme control or required account setting.
The page flashes light, then turns dark The preference was applied after navigation or the page updates asynchronously. Set colorScheme: 'dark' on the context before navigation, or wait for a known dark-theme element/state before capture.
Screenshot is blank or incomplete The page has not rendered its main content when capture starts. Wait for a page-specific selector to become visible and verify that the selector is present on the target page.
Content below the fold is missing The capture used the viewport dimensions only. Pass fullPage: true to Playwright’s screenshot call.
The capture differs at another screen size The viewport changed responsive breakpoints or layout. Set explicit viewport width and height in the browser context and keep them fixed between runs.
The hosted API request errors The key, URL encoding, or requested parameters may be invalid. Check the access key, encode the target URL, and compare parameter names and values with the current API documentation. Inspect the response status and headers.

FAQ

Does dark mode emulation change the website permanently?

No. It changes the browser’s media preference for that page or context. It does not alter the site’s files or settings outside that browser session.

Can I make every website render a dark theme?

No. Emulation exposes a preference; each website decides whether and how to respond. A site-specific theme control may be necessary.

Should I use a browser library or an API?

Use Playwright when you need direct control of browser context, navigation, or interactions. Use a hosted screenshot API when you prefer a request-based capture workflow and its available options fit the task.