How to Fix a Website Screenshot That Is Too Dark in Headless Chrome
A dark screenshot often means the page is using its dark color scheme. Set and verify the browser’s color preference, then control the capture environment and page state.
A website screenshot that looks too dark may be capturing the page in its dark color scheme. Set the browser’s prefers-color-scheme preference to light before capture, and verify what the page sees. If the result still differs from your expected image, align the browser and host environment and wait for the page to reach the state you intend to capture.
This guide uses Playwright for the main fix and explains the related Puppeteer setting that is often mistaken for a theme control. For a managed screenshot option, see ScreenshotNeo.
1. Check whether the page is using dark mode
Sites can use CSS media queries such as @media (prefers-color-scheme: dark) to render a dark theme. A browser automation session may therefore capture a dark page when that preference is active. Check both media queries inside the page before changing unrelated screenshot settings.
const colorState = await page.evaluate(() => ({
dark: matchMedia('(prefers-color-scheme: dark)').matches,
light: matchMedia('(prefers-color-scheme: light)').matches,
}));
console.log(colorState);
If dark is true, the page is being told that the dark scheme is preferred. If light is true, the light scheme is preferred. Use these values to distinguish a theme preference issue from a transparent background or a rendering difference.
2. Set light mode in Playwright
Choose the scope that fits the script. Set colorScheme: 'light' on the browser context when all pages in that context should use light mode. Call page.emulateMedia({ colorScheme: 'light' }) when you need to control or change the preference for a particular page. Apply the preference before capturing; then query matchMedia to confirm it.
Complete Playwright example
Install Playwright and its browser binaries as described in the Playwright installation guide. Save this as an ES module, for example screenshot.mjs, then run it with a target URL as the first argument:
import { chromium } from 'playwright';
const url = process.argv[2];
if (!url) {
throw new Error('Usage: node screenshot.mjs https://example.com');
}
const browser = await chromium.launch();
try {
const context = await browser.newContext({
colorScheme: 'light',
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
const page = await context.newPage();
await page.goto(url, { waitUntil: 'networkidle' });
const colorState = await page.evaluate(() => ({
dark: matchMedia('(prefers-color-scheme: dark)').matches,
light: matchMedia('(prefers-color-scheme: light)').matches,
}));
console.log('Page color preference:', colorState);
await page.screenshot({ path: 'page.png', fullPage: true });
await context.close();
} finally {
await browser.close();
}
The combined example is an editorial synthesis of documented Playwright APIs; adapt its navigation wait and viewport to your site. networkidle is a useful starting point, not a guarantee that every page has finished its own animations, delayed content, or application work.
Switch a page-level preference
For an existing page, emulate the preference before capture:
await page.emulateMedia({ colorScheme: 'light' });
const isLight = await page.evaluate(() =>
matchMedia('(prefers-color-scheme: light)').matches
);
if (!isLight) throw new Error('The page did not receive light color scheme');
await page.screenshot({ path: 'page.png' });
Playwright documents light and dark color scheme emulation. Passing null resets color-scheme emulation. The Page API marks no-preference as deprecated; use an explicit scheme when you need reproducible results. See the Playwright Page API and BrowserType API.
3. Puppeteer: do not use transparency as a theme fix
Puppeteer’s omitBackground screenshot option removes the default background so the output can be transparent. It does not set the page’s light or dark color scheme, and it does not apply to JPEG. Leave it off when you want the page’s normal background. To fix a dark site theme, emulate the color scheme through the browser’s media feature before taking the screenshot.
import puppeteer from 'puppeteer';
const url = process.argv[2];
if (!url) throw new Error('Usage: node screenshot.mjs https://example.com');
const browser = await puppeteer.launch();
try {
const page = await browser.newPage();
await page.emulateMediaFeatures([
{ name: 'prefers-color-scheme', value: 'light' },
]);
await page.goto(url, { waitUntil: 'networkidle2' });
const colorState = await page.evaluate(() => ({
dark: matchMedia('(prefers-color-scheme: dark)').matches,
light: matchMedia('(prefers-color-scheme: light)').matches,
}));
console.log('Page color preference:', colorState);
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
Use the Puppeteer ScreenshotOptions reference for screenshot options, including omitBackground. For navigation and capture patterns, see the Puppeteer screenshots guide.
4. Make screenshot comparisons reproducible
Once the page receives the intended scheme, a screenshot may still differ from a baseline because rendering depends on the capture environment. Playwright identifies the host operating system, browser version, settings, hardware, power source, headless mode, and other factors as sources of variation. Keep these conditions consistent when comparing images, especially in visual regression checks. See Playwright’s visual comparisons guidance.
- Use the same browser engine and version as the baseline.
- Run comparisons on the same operating system and with consistent browser settings.
- Keep headed or headless mode consistent.
- Set the viewport and
deviceScaleFactorexplicitly. Playwright documents a default device scale factor of1; it changes pixel sampling and output dimensions, not the page’s light/dark preference. - Keep the page’s color scheme explicit rather than relying on the environment’s default.
Avoid “correcting” brightness after capture until you know the page received the intended theme. Image-level changes can conceal the cause and may produce the wrong result for images, shadows, and other page content.
5. Wait for the intended page state
Set the color scheme before navigation when possible, then wait for the page to load and for any site-specific content to settle. Puppeteer’s screenshot guide demonstrates navigation with waitUntil: 'networkidle2' before capture. That is an example, not a universal completion signal: pages with polling, long-lived requests, lazy loading, or client-side updates may need a more specific condition.
For a page you control, wait for a meaningful selector or application-ready state. If a particular component changes theme after hydration, wait for that component to render before capturing. Avoid assuming that one fixed delay works for every website.
6. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. A single GET request can return a screenshot in PNG, JPEG, or WebP, or a PDF. The API documents screenshot options at ScreenshotNeo Docs.
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,
)
r.raise_for_status()
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}`);
const bytes = new Uint8Array(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', bytes));
Before capture, ScreenshotNeo accepts the cookie or consent banner like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers report the page verdict and billing status. Its MCP server provides take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients. 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.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Screenshot is still dark after setting light mode | The preference was set after the page rendered, or the site applies its own theme setting. | Set the context preference before navigation or emulate it before capture. Check both media queries, then inspect the site’s own theme controls or stored preference. |
matchMedia('(prefers-color-scheme: light)') is false |
The page context has not received the light preference. | Set colorScheme: 'light' on the context or call page.emulateMedia({ colorScheme: 'light' }), then recheck. |
Setting omitBackground changes the image unexpectedly |
That option makes the default background transparent; it does not choose a theme. | Remove omitBackground unless transparency is intended. Set the color scheme separately. |
| Capture is blank or partly rendered | Navigation completion did not mean that the site’s content was ready, or the request failed. | Check navigation errors and wait for the relevant selector or application state. Treat network-idle waits as a starting point rather than proof of readiness. |
| Local and CI screenshots differ | Browser version, OS, settings, hardware, power conditions, or headless mode differ. | Align the baseline and comparison environments and set viewport, device scale factor, and color scheme explicitly. |
| Screenshot dimensions differ | Viewport or device scale factor differs. | Set both explicitly and compare captures made with the same values. Device scale factor changes pixel dimensions and sampling, not the page theme. |
8. Performance, reliability, and cost
For a local Playwright or Puppeteer workflow, browser launch and page loading are part of the capture job. Reuse a browser process when capturing multiple pages in one run, while creating a fresh context when pages need isolated settings. A navigation wait can improve consistency but may take longer or fail to settle on pages that keep network connections open; prefer a page-specific readiness condition when available.
Visual comparison is most reliable when the browser and host setup are controlled. A fixed viewport and explicit light preference remove two sources of variation, while matching the baseline environment reduces differences from platform rendering. The research sources provide no universal timing target, performance benchmark, or estimate of the cost of a local capture, so measure your own pages and infrastructure.
ScreenshotNeo offers a free tier of 1,000 screenshots per month without a card, then Starter at $5 for 3,000, Growth at $15 for 15,000, Pro at $39 for 60,000, Scale at $99 for 250,000, and Business at $249 for 1,000,000. Yearly billing gives two months free, and every feature is available on every plan. Only clean shots are billed; the response includes X-Page-Verdict and X-Billed headers to indicate the outcome.
FAQ
Does headless Chrome always use dark mode?
No. Check the page’s media-query result and explicitly set the intended scheme. Headless mode alone does not tell you which theme the site rendered.
Should I use no-preference?
For a reproducible capture, choose light or dark. Playwright’s Page API marks no-preference as deprecated.
Will a larger device scale factor make the screenshot brighter?
No. It controls device pixel scale and can affect output dimensions and sampling. It is not a color-scheme setting.
Can I use ScreenshotNeo from an AI agent?
Yes. Its MCP server exposes screenshot, page information, and PDF capture tools to Claude, Cursor, and other MCP clients.


