ScreenshotNeo

BlogHow-to

How to Capture a Website Screenshot in Dark Mode with BrowserCat

Use BrowserCat with Playwright to request dark mode before navigation, verify the page received that preference, and save a screenshot. Includes runnable code and fixes for common capture issues.

By the ScreenshotNeo team4 October 20268 min read

Direct answer: connect Playwright to BrowserCat’s cloud Chromium endpoint, set the page’s color scheme to dark before opening the target, then save the rendered page with page.screenshot(). This asks the site to use dark styles; it does not recolor a site that has no dark theme or that relies on its own theme switch.

The workflow is BrowserCat’s documented Playwright connection pattern combined with Playwright’s dark color-scheme emulation and screenshot APIs. BrowserCat documents a cloud Chromium session and the endpoint wss://api.browsercat.com/connect; Playwright documents page.emulateMedia({ colorScheme: 'dark' }) and screenshot options. This is a documentation-based implementation pattern, not a claim of testing against a particular website. See the BrowserCat quick start, Playwright emulation guide, and Page API.

1. Create a BrowserCat screenshot script

Prerequisites

  • Node.js with npm.
  • A BrowserCat account and API key. Keep the key in an environment variable; do not put it in source code or commit it.
  • Network access to BrowserCat’s WebSocket endpoint and to the target site.

Install the Playwright client library:

npm install playwright-core

Set the key in your shell, then save the following as capture-dark.mjs:

export BROWSERCAT_API_KEY="YOUR_API_KEY"
node capture-dark.mjs "https://example.com"
import * as pw from 'playwright-core';

const url = process.argv[2];
if (!url) {
  throw new Error('Usage: node capture-dark.mjs https://example.com');
}
if (!process.env.BROWSERCAT_API_KEY) {
  throw new Error('Set BROWSERCAT_API_KEY before running this script.');
}

async function captureDark(targetUrl) {
  const browser = await pw.chromium.connect(
    'wss://api.browsercat.com/connect',
    { headers: { 'Api-Key': process.env.BROWSERCAT_API_KEY } }
  );

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

    // Setting this before navigation lets the page see the preference on startup.
    await page.emulateMedia({ colorScheme: 'dark' });

    const response = await page.goto(targetUrl, {
      waitUntil: 'networkidle',
      timeout: 60000
    });

    const preferenceReceived = await page.evaluate(
      () => matchMedia('(prefers-color-scheme: dark)').matches
    );
    console.log({
      status: response?.status(),
      preferenceReceived,
      title: await page.title()
    });

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

captureDark(url).catch(error => {
  console.error(error);
  process.exitCode = 1;
});

The script opens a fresh browser context, requests dark mode, navigates, logs the HTTP status and preference check, and writes website-dark.png. BrowserCat’s quick start shows connecting with Playwright’s chromium.connect() and an Api-Key header; it describes its cloud sessions as Chromium in “new” headless mode. The BrowserCat setup steps are documented in its quick start.

2. Confirm the website actually rendered dark styling

matchMedia('(prefers-color-scheme: dark)').matches confirms that the page can detect a dark preference. It does not inspect the page’s colors or prove that its components adopted a dark theme. Check the screenshot itself, or inspect a representative element’s computed styles if you need an automated assertion.

const appearance = await page.evaluate(() => {
  const main = document.querySelector('main') || document.body;
  return {
    preference: matchMedia('(prefers-color-scheme: dark)').matches,
    background: getComputedStyle(main).backgroundColor,
    color: getComputedStyle(main).color
  };
});
console.log(appearance);

Computed colors can be transparent or inherited, so treat this as a diagnostic rather than a universal test for “dark.” For a reliable visual check, compare the captured page with the site’s expected theme and verify the regions that matter to your use case.

3. Choose the right wait and screenshot settings

Need Playwright option or approach Trade-off
Capture the current viewport Omit fullPage, or set it to false. Fast and bounded, but content below the fold is omitted.
Capture a long page fullPage: true Includes the full scrollable page; very tall pages can take longer and produce large files.
Capture one component await page.locator('.report').screenshot({ path: 'report.png' }) Useful for a chart, card, or other element; the selector must resolve to the intended visible element.
Wait for app content await page.locator('main').waitFor({ state: 'visible' }) More targeted than waiting for every network connection to become idle.
Wait briefly for animation or delayed UI await page.waitForTimeout(1000) Simple but adds fixed delay; prefer a meaningful selector when possible.
Reduce animation variation animations: 'disabled' in screenshot options Can make captures more stable; page scripts or timed content can still vary.
Change preference after navigation await page.emulateMedia({ colorScheme: 'dark' }), then wait or reload if required Some applications only select a theme during startup or store a separate theme choice.

Playwright supports full-page screenshots and locator screenshots in its Page API. For capture consistency, choose a fixed viewport, use a fresh context for each independent theme, and wait for a stable page element instead of adding a long arbitrary delay.

Context setting versus page setting

You can set the preference when creating the context, as in browser.newContext({ colorScheme: 'dark' }), or set it on a page with page.emulateMedia({ colorScheme: 'dark' }). The context option applies to pages created in that context. Setting it before navigation makes the preference available during initial page setup. Playwright also documents media emulation in its emulation guide.

Other output choices

Playwright’s screenshot API supports image output and options such as a file path, full-page capture, and animation handling. Select the format and output settings supported by the API version you install, and check its current screenshot API reference for the complete option list. This article’s runnable example saves PNG output.

4. Troubleshooting

Symptom Likely cause Fix
Screenshot is still light, but the preference check is true The site has no dark color-scheme styles, uses a separate in-app appearance control, or has saved account or local storage theme state. Use the site’s own theme control when available. If the theme is stored in page state, configure that state intentionally before capture. Do not assume media emulation changes arbitrary colors.
Preference check is false The emulation was not applied to the page/context used for navigation, or another setup path replaced the context. Set colorScheme: 'dark' on the context or call page.emulateMedia() on the actual page before navigation; log the check again.
WebSocket connection fails or is rejected Missing or invalid API key, incorrect endpoint, or a network policy blocking WebSockets. Confirm BROWSERCAT_API_KEY is present and valid, use wss://api.browsercat.com/connect, and check outbound WebSocket access. BrowserCat’s documented connection pattern is in its quick start.
Cannot find package 'playwright-core' The package is not installed in the working project or the script runs from another project directory. Run npm install playwright-core in the project directory and invoke the script there.
Navigation times out on a site that looks loaded networkidle can be delayed by analytics, polling, or long-lived requests. Use a less strict navigation wait such as domcontentloaded, then wait for a page-specific visible selector before capturing. Keep a finite timeout and handle failures.
Screenshot misses content or captures a loading state Client-side rendering or lazy content had not completed when the image was taken. Wait for the relevant content selector; scroll or otherwise trigger lazy loading if needed, then capture. Use full-page mode only after content is present.
Cookie dialog or popup obscures the page The page presented an overlay that the script did not handle. Dismiss it through the site’s normal controls when appropriate, or use a capture workflow that handles consent overlays. A popup may appear after initial load, so wait for the capture state you need.
Colors differ between runs Dynamic content, animation, rotating banners, time-dependent data, viewport differences, or account state changed. Fix the viewport and relevant state, disable animations in screenshot capture, wait on stable selectors, and avoid relying on volatile page regions.
Screenshot file exists but is blank or incomplete Navigation may have failed, rendered a bot check, or captured before the page content appeared. Log the navigation response status and page title, inspect the output, and wait for a meaningful content selector. Handle access challenges according to the site’s policies.

5. Reliability, performance, and cost considerations

  • Use explicit readiness conditions. A selector that represents the content you need is usually a better signal than a fixed sleep. Some pages never become truly idle because they keep connections open.
  • Set operational timeouts. Navigation and selector waits should have finite limits. On failure, preserve enough logs to distinguish a connection issue, navigation error, and missing content.
  • Keep captures reproducible. Use a known viewport and consistent page state. Dark preference alone cannot control user-specific theme settings, server-selected content, or third-party widgets.
  • Control capture size. Full-page images of long pages use more time and storage than viewport or element captures. Choose the smallest capture area that meets the need.
  • Close remote sessions. The finally block closes the browser even when navigation or capture throws, which helps avoid leaving sessions open after errors.
  • Plan costs from your BrowserCat account terms. The sources used here document how to connect and point to BrowserCat’s dashboard for usage and billing; they do not establish a price or per-screenshot cost. Check the current BrowserCat dashboard and pricing before estimating a workload.

BrowserCat’s documentation lists Chromium and Chrome as supported and says Firefox and WebKit are on its roadmap. Do not assume those other browser backends are available for this workflow; consult the current BrowserCat browser configuration documentation.

6. Or skip the browser setup

If you only need a screenshot file and do not need to control the browser with Playwright, ScreenshotNeo provides a website screenshot API and MCP server. One GET request returns an image or PDF. Add a format parameter such as format=webp when you want WebP output.

cURL:

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

Python:

import requests

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

Node.js:

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(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));

See the ScreenshotNeo API documentation for request options and response details. Cookie banners, newsletter popups, and chat widgets are removed before capture; each step can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers identify the page verdict and billing status. Its MCP server lets AI agents use screenshot, page-info, and PDF tools. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month with no card.

FAQ

Does dark color-scheme emulation force every website into dark mode?

No. It exposes a dark preference to the page. A site must have dark styling or a separate theme mechanism that the capture flow can use.

Can I use this for a single element instead of the whole page?

Yes. Use a Playwright locator’s screenshot method for the target element, and make sure the selector identifies the intended element.

Does the preference check prove the screenshot is dark?

No. It verifies the preference reported to page JavaScript. Inspect the output or test the actual theme styles separately.

Can I use BrowserCat’s browser configuration header to set dark mode?

The documented workflow here sets dark mode through Playwright’s media emulation API. BrowserCat documents its configuration header and query options separately; the reviewed documentation does not establish a BrowserCat-specific dark-mode setting.

Will this capture match a signed-in visitor’s theme?

Only if the relevant authentication and theme state are also present in the browser context. A fresh context starts without the user’s existing site session.