How to Enable Dark Mode on Websites With CSS and Browser Tools
Add a standards-based dark theme with CSS, emulate it in Chrome, Firefox, and Edge, and verify screenshots without changing your system settings.
Direct answer: declare color-scheme: light dark, define your light colors, and override them inside @media (prefers-color-scheme: dark). Then use your browser’s DevTools rendering or emulation controls to force light or dark mode without changing the operating-system setting.
1. Add a standards-based dark theme
The prefers-color-scheme media feature reports whether the user agent or operating system prefers light or dark colors. The color-scheme property tells the browser that your page supports both schemes, including native controls, the canvas, and scrollbars.
Minimal HTML
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<link rel="stylesheet" href="styles.css">
<title>Dark mode demo</title>
</head>
<body>
<main class="card">
<h1>System-aware dark mode</h1>
<p>This page follows the browser's preferred color scheme.</p>
<button type="button">Continue</button>
</main>
</body>
</html>
Complete CSS
:root {
color-scheme: light dark;
--bg: #ffffff;
--fg: #1f2328;
--surface: #f6f8fa;
--border: #d0d7de;
--accent: #0969da;
--accent-fg: #ffffff;
--focus: #8250df;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
min-height: 100vh;
color: var(--fg);
background: var(--bg);
font: 16px/1.5 system-ui, sans-serif;
}
.card {
max-width: 42rem;
margin: 4rem auto;
padding: 2rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 0.75rem;
}
button {
color: var(--accent-fg);
background: var(--accent);
border: 0;
border-radius: 0.4rem;
padding: 0.65rem 1rem;
cursor: pointer;
}
button:focus-visible {
outline: 3px solid var(--focus);
outline-offset: 2px;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0d1117;
--fg: #e6edf3;
--surface: #161b22;
--border: #30363d;
--accent: #2f81f7;
--accent-fg: #ffffff;
--focus: #a371f7;
}
}
Keep explicit colors for your own components. color-scheme helps native browser surfaces, but it does not recolor your cards, images, SVG artwork, or third-party widgets.
2. Test dark mode without changing the computer theme
Chrome
- Open DevTools with F12 or Ctrl/Cmd+Shift+I.
- Open the Command Menu and run Show Rendering, or open the Rendering panel from the DevTools menu.
- Under Emulate CSS media feature prefers-color-scheme, choose
prefers-color-scheme: darkorprefers-color-scheme: light. - Reload the page and inspect every state.
Chrome’s Rendering tool changes the emulated media preference. It does not change the operating-system preference.
Firefox
- Open Developer Tools and inspect an element.
- Open the color-scheme emulation controls in the Inspector’s emulation panel.
- Choose Light, Dark, or Disabled.
Disabled mode lets the operating-system value drive prefers-color-scheme.
Edge
Open DevTools’ Rendering tool and select Emulate CSS prefers-color-scheme: dark or light. Refresh to exercise the page with that preference.
3. Verify every visual state
- Default light and dark colors, including text, backgrounds, borders, and shadows.
- Hover, focus-visible, active, disabled, and validation-error states.
- Dialogs, menus, tooltips, dropdowns, and other overlays.
- Form controls and scrollbars supplied by the browser.
- Inline SVG, external SVG, icons, photographs, and transparent PNG assets.
- Embedded iframes and third-party widgets, which may have their own color-scheme behavior.
- Contrast at normal and high zoom levels.
Use semantic variables rather than scattering literal colors through selectors. This makes the dark palette auditable and prevents one component from retaining a light-only value.
4. Understand Chrome Automatic dark mode
Chrome’s Automatic dark mode is a generated transformation applied by the browser. It is separate from the dark theme you author with CSS, and while it is enabled Chrome sets prefers-color-scheme to dark. Turn Automatic dark mode off when diagnosing whether your own stylesheet caused a visual change. Test your CSS emulation and the generated transformation as separate cases.
5. Optional theme control with JavaScript
A system-aware theme needs no JavaScript. Add a manual switch only when your product requires an explicit user choice. Store the choice and apply an attribute that overrides the system value.
const button = document.querySelector('[data-theme-toggle]');
const saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.dataset.theme = saved;
}
button?.addEventListener('click', () => {
const current = document.documentElement.dataset.theme;
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = next;
localStorage.setItem('theme', next);
});
:root[data-theme="light"] {
color-scheme: light;
--bg: #ffffff;
--fg: #1f2328;
--surface: #f6f8fa;
--border: #d0d7de;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #0d1117;
--fg: #e6edf3;
--surface: #161b22;
--border: #30363d;
}
Decide how a saved choice interacts with later operating-system changes. A common policy is: no saved value means follow the system; a saved value means honor the user’s explicit choice.
6. Capture and compare dark-mode screenshots
For repeatable visual checks, capture the same URL once with a light preference and once with a dark preference. Keep viewport, device scale, fonts, content data, and wait conditions identical so differences represent the theme rather than layout timing.
Or skip the browser setup
ScreenshotNeo can request a dark-mode screenshot through one API call. Its documentation lists the capture options, including dark mode, viewport and device settings, waits, custom CSS, and output formats.
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://stripe.com \
-d dark_mode=true \
-o shot.webp
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={
"access_key": "YOUR_API_KEY",
"url": "https://stripe.com",
"dark_mode": "true",
},
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',
dark_mode: 'true'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot failed: ${res.status}`);
const image = Buffer.from(await res.arrayBuffer());
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', image));
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response reports the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The free plan includes 1,000 screenshots each month without a card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account.
7. Performance, reliability, and cost considerations
- CSS performance: keep both palettes in variables so the browser changes values without duplicating component rules.
- First paint: place the color-scheme meta tag in the document head to reduce mismatches while styles load.
- Assets: use SVGs and images that remain legible on both backgrounds; avoid baking a white background into transparent artwork.
- Screenshot reliability: wait for a key selector, a known delay, or network idle before capture when the page loads theme-dependent content.
- Repeatability: pin viewport, device scale, timezone, geolocation, custom fonts, and content fixtures for visual comparisons.
- Cost: local DevTools testing has no service cost. ScreenshotNeo bills only clean shots; cache hits and failed or unusable captures are not billed.
8. Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
@media (prefers-color-scheme: dark) never matches |
The browser is still using the light preference, or DevTools emulation is disabled. | Set dark emulation in the Rendering or Inspector panel, then reload. Check that another selector is not overriding the variables. |
| Native controls stay light | color-scheme is missing or declared only on a nested component. |
Declare color-scheme: light dark on :root and include the matching meta tag. |
| The page becomes dark even though your CSS does not | Chrome Automatic dark mode is applying a generated transformation. | Disable Automatic dark mode and retest with explicit CSS media emulation. |
| Some cards or text remain light | A component uses hard-coded colors instead of theme variables. | Move backgrounds, text, borders, shadows, and focus colors into semantic custom properties. |
| Icons disappear in dark mode | Monochrome artwork has insufficient contrast or assumes a light background. | Use current-color SVGs or provide dark-mode asset variants and inspect transparent edges. |
| Screenshot shows a popup or consent banner | The capture happened before the overlay was dismissed, or the tool does not remove it. | Wait for a stable selector and hide or dismiss the overlay. ScreenshotNeo removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. |
| Screenshot is blank or times out | The page failed to load, requires authentication, or is blocked by a bot check. | Check the target URL, headers, cookies, and network access; wait for a meaningful selector. With ScreenshotNeo, unusable captures are reported and are not billed. |
9. Compatibility limits
MDN records prefers-color-scheme as broadly available across browsers since January 2020, with exact behavior depending on the browser versions you support. Check the MDN compatibility table before dropping older browser fallbacks.
FAQ
Can I make dark mode the default?
Yes. Author the dark palette as the root values and use @media (prefers-color-scheme: light) for the light override. Keep color-scheme: light dark accurate.
Does color-scheme recolor my images?
No. It affects browser-provided surfaces and controls. Choose image, SVG, and component colors yourself.
Do I need JavaScript to detect dark mode?
No. CSS responds directly to the media feature. JavaScript is useful for a persistent manual override or for reacting to preference changes in application logic.
Why do screenshots differ from what I see in DevTools?
Compare the preference source, Automatic dark mode setting, viewport, device scale, fonts, loading state, and overlays. A generated browser transformation is a different test from your authored CSS.


