How to Schedule Website Screenshots in Dark Mode
Schedule repeatable dark-mode website screenshots with Playwright and GitHub Actions. Set the color scheme before capture, save the results, and handle common automation issues.
To schedule website screenshots in dark mode, use Playwright to emulate the browser’s dark color scheme, save the screenshot, and run the script on a recurring schedule. This guide uses GitHub Actions; the same capture script can also run from a machine or another scheduler you operate.
Setting colorScheme: 'dark' emulates the prefers-color-scheme: dark media feature. A site must support that preference to change its appearance. If it uses a saved theme toggle instead, automate that control too. See Microsoft Playwright’s color scheme emulation documentation.
1. Create a Playwright screenshot script
This runnable JavaScript example captures a full-page PNG for each URL in a list. It sets dark mode before navigation, uses a fixed viewport, applies a timeout, creates the output directory, and closes the browser even if capture fails.
const { chromium } = require('playwright');
const fs = require('node:fs/promises');
const path = require('node:path');
const urls = [
'https://example.com',
];
const outputDir = path.resolve('screenshots');
const safeName = (url) => {
const parsed = new URL(url);
return `${parsed.hostname.replace(/[^a-z0-9.-]/gi, '-')}.png`;
};
(async () => {
await fs.mkdir(outputDir, { recursive: true });
const browser = await chromium.launch();
try {
const context = await browser.newContext({
colorScheme: 'dark',
viewport: { width: 1440, height: 900 },
deviceScaleFactor: 1,
});
try {
const page = await context.newPage();
page.setDefaultNavigationTimeout(30_000);
for (const url of urls) {
try {
const response = await page.goto(url, { waitUntil: 'domcontentloaded' });
if (!response || !response.ok()) {
throw new Error(`Navigation failed${response ? `: HTTP ${response.status()}` : ''}`);
}
await page.screenshot({
path: path.join(outputDir, safeName(url)),
fullPage: true,
type: 'png',
animations: 'disabled',
});
console.log(`Saved ${url}`);
} catch (error) {
console.error(`Could not capture ${url}:`, error.message);
process.exitCode = 1;
}
}
} finally {
await context.close();
}
} finally {
await browser.close();
}
})();
Save this as capture.js. Install Playwright and its Chromium browser in the project:
npm init -y
npm install --save-dev playwright
npx playwright install chromium
Run it with node capture.js. Screenshots are written to screenshots/. The script exits unsuccessfully if any URL fails, which lets the scheduler report a failed run while still attempting later URLs.
Viewport or full page
fullPage: true captures the full scrollable page; omit it for a viewport screenshot. Very tall pages can take longer and create large images. For a specific region, use page.locator('your-selector').screenshot({ path: 'element.png' }). The Playwright Page API documents screenshot paths, full-page capture, file type, scaling, masks, animation handling, and stylesheets.
Wait for the page you need
The example waits for domcontentloaded so it does not wait indefinitely for every network request. If the page renders important content later, wait for a stable selector before taking the screenshot:
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.locator('main article').waitFor({ state: 'visible', timeout: 15_000 });
await page.screenshot({ path: 'page.png', fullPage: true });
Use waitUntil: 'load' when the page’s load event is a useful boundary. networkidle can help with pages that finish loading requests, but analytics, polling, and long-lived connections may prevent it from becoming idle. Prefer a selector that represents the content you need.
2. Tune the capture for your pages
Keep the browser version, operating system, viewport, device scale, and headless setting consistent if you compare screenshots over time. Playwright notes that rendering can vary with the host OS, browser version, settings, hardware, power source, and headless mode; its visual comparison guidance recommends using the same environment as the baseline.
| Need | Playwright setting or method | Notes |
|---|---|---|
| Dark preference | browser.newContext({ colorScheme: 'dark' }) |
Set on the context before creating pages or navigating. |
| Change preference later | page.emulateMedia({ colorScheme: 'dark' }) |
Emulates the media feature on that page. |
| Viewport dimensions | viewport: { width, height } |
Use the same dimensions for comparable output. |
| Retina-like output | deviceScaleFactor: 2 |
Increases pixel dimensions and file size; CSS layout dimensions remain the viewport. |
| Output format | type: 'png', 'jpeg', or 'webp' |
Choose a supported format suitable for your workflow; quality applies to lossy formats. |
| Reduce animation differences | animations: 'disabled' |
Useful for repeat captures with animated elements. |
| Cover volatile content | mask: [locator] or screenshot stylesheet |
Use masks or styles to obscure timestamps, rotating content, or other unstable regions. |
| Capture one element | locator.screenshot() |
Wait for the locator to be visible first. |
For pages with a site-specific theme switch, locate the control, click it, and wait for the theme to settle. The browser preference alone cannot set a theme stored in local storage or an account setting. For authenticated pages, provision credentials through the runner’s secret store and use an approved login or saved browser state; never commit credentials or session files to the repository.
3. Schedule the script with GitHub Actions
Create .github/workflows/dark-screenshots.yml and commit it to the repository’s default branch. This example runs daily at 06:17 UTC and can also be started manually from the Actions interface.
name: Scheduled dark screenshots
on:
workflow_dispatch:
schedule:
- cron: '17 6 * * *'
jobs:
capture:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install --with-deps chromium
- run: node capture.js
- name: Upload screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: dark-screenshots
path: screenshots/
if-no-files-found: ignore
Commit package.json and package-lock.json so npm ci can install the pinned dependency tree. If you need private pages, define credentials in GitHub Actions secrets and pass them to the script through environment variables. Choose an artifact retention policy that fits your project and check the platform’s current limits and settings.
GitHub Actions scheduled workflows use POSIX cron syntax. GitHub documents UTC as the default timezone, optional IANA timezone support, and a shortest interval of five minutes. Schedules run against the latest commit on the default branch; the workflow file must be on that branch. GitHub warns that busy periods, especially near the start of an hour, can delay or drop scheduled runs, so choose an off-hour minute when practical. Public repository schedules are disabled after 60 days without repository activity. See GitHub’s schedule event documentation.
Choose a scheduler that fits the timing
GitHub Actions is convenient when the capture belongs with a repository and a delayed run is acceptable. For tighter timing or infrastructure-specific access, run the same command from a machine or scheduler your team operates. For example, a Unix cron entry for 06:17 UTC is:
17 6 * * * cd /path/to/project && /usr/bin/node capture.js >> capture.log 2>&1
Ensure the machine has the required Playwright browser dependencies, a stable timezone, network access to the target pages, and a process supervisor or log rotation appropriate to your environment. A scheduler starts a process; it does not by itself guarantee successful navigation or preserve output.
4. Handle output, multiple pages, and failures
- Use predictable names: include the hostname and, if capturing multiple views, a viewport or date suffix. Avoid names derived from arbitrary URL text without sanitizing them.
- Decide what “daily” means: GitHub’s default is UTC. If local time matters, use the documented timezone option or convert the desired time to UTC and account for daylight-saving changes.
- Retain and deliver files: upload artifacts, copy files to storage, or send them to your own notification system. Configure retention and access separately; generated screenshots may contain private data.
- Keep runs isolated: use a fresh context per site when cookies, authentication, or local storage should not leak between targets.
- Control page volatility: disable animations, mask changing regions, and wait for the relevant content rather than an arbitrary long delay.
- Retry selectively: transient DNS, network, or server errors may be retried with a bounded delay. Do not endlessly retry deterministic errors such as a missing selector or invalid URL.
5. Troubleshoot common problems
| Symptom | Likely cause | Fix |
|---|---|---|
| The screenshot is still light | The site ignores prefers-color-scheme, or a stored theme setting overrides it. |
Inspect the site’s theme behavior. Click its theme control or configure the supported setting, then wait for the page to update. |
| Screenshot is blank or missing content | Capture ran before client-side rendering or lazy content appeared. | Wait for a visible content selector. Scroll or otherwise trigger lazy loading when the page requires it. |
| Navigation times out | Slow server, blocked network, or waiting for a page state that never arrives. | Use a suitable navigation event, set a bounded timeout, and wait for a target selector. Check runner access and the target response. |
| Browser executable not found | Playwright package is installed but its browser was not installed in the environment. | Run npx playwright install chromium; in Linux CI, install dependencies with npx playwright install --with-deps chromium. |
| Images differ between runs | Browser or host changes, animations, timestamps, ads, or dynamic content. | Pin the runtime and browser setup, keep viewport and scale fixed, disable animations, and mask or style volatile regions. |
| Scheduled job does not run | Workflow is not on the default branch, schedule is delayed, or a public repository has been inactive for 60 days. | Check the default branch and Actions status, choose a non-top-of-hour minute, and restore repository activity if schedules were disabled. |
| No artifact appears | The script wrote files elsewhere, failed before capture, or no screenshots directory existed. | Check logs and output paths; create the directory before capture and inspect the upload step’s no-files behavior. |
| Login page captured instead of target | Session was absent, expired, or rejected by the site. | Provide valid credentials through secrets, renew the approved session setup, and verify the authenticated destination before capture. |
6. Performance, reliability, and cost
Capture time and storage depend on page weight, browser startup, network conditions, full-page length, and output dimensions. Reuse a browser process for several URLs in one run, but use separate contexts when page state must be isolated. Full-page and high device-scale screenshots increase memory use and file size; capture only the area and resolution you need. Keep timeouts bounded and log each URL so one failure is diagnosable.
GitHub Actions scheduling is suitable for recurring jobs that tolerate delayed execution, not an exact-time guarantee. For comparison work, rendering consistency depends on keeping the host and browser environment stable. Compute cost includes runner time and whatever storage or delivery service you choose; this research does not establish a specific price for those services. Estimate from your actual page set and retention needs rather than assuming a fixed cost per screenshot.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request can return a screenshot as PNG, JPEG, WebP, or PDF. Its dark mode option requests a dark appearance for the page. See the ScreenshotNeo API documentation for request options and setup.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-d dark_mode=true \
-o example-dark.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://example.com",
"dark_mode": "true",
},
timeout=90,
)
r.raise_for_status()
open("example-dark.webp", "wb").write(r.content)
const q = new URLSearchParams({
access_key: 'YOUR_API_KEY',
url: 'https://example.com',
dark_mode: 'true',
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('example-dark.webp', Buffer.from(await res.arrayBuffer()));
To make this recurring, schedule the request with the same GitHub Actions workflow or scheduler described above. ScreenshotNeo accepts cookie consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, get page information, and capture PDFs. The Free plan includes 1,000 screenshots monthly with no card; paid plans start at $5 for 3,000 screenshots.
Start free with 1,000 screenshots a month and no card.
Frequently asked questions
Does dark mode change the saved screenshot after capture?
No. Playwright emulates the browser’s dark color preference before rendering, and the screenshot captures the resulting page.
Can I schedule more than one URL?
Yes. Add URLs to the script’s list, use a stable filename per page, and decide whether each page needs a separate browser context.
Will the schedule run at the exact minute?
Not necessarily. GitHub documents that scheduled runs can be delayed or dropped during high load. Use a scheduler with guarantees suited to your timing requirements.
Does every site have a dark theme?
No. The emulated preference only signals the browser setting; each site decides whether to implement a dark appearance.


