Dark mode has gone from a niche preference to something a large share of visitors expect — or at least appreciate — especially on a site they browse at night. The good news is that CSS can detect a visitor's operating-system-level preference and switch styles automatically, with no JavaScript, no toggle button to build, and no flash of the wrong theme on page load.
The prefers-color-scheme Media Query
prefers-color-scheme is a media feature that reflects whether the visitor's OS or browser is set to light or dark mode. It works exactly like any other media query — you wrap dark-specific rules inside it.
body {
background: #ffffff;
color: #111827;
}
@media (prefers-color-scheme: dark) {
body {
background: #0f172a;
color: #e2e8f0;
}
}
On a device set to dark mode, the second rule block wins and the page renders with a dark background and light text, automatically, the very first time the page loads — no flicker, because there's no JavaScript waiting to run first.
The Cleaner Approach: Variables + One Override Block
Writing a dark-mode override for every single rule in a stylesheet doesn't scale. The practical pattern is to define your colors as CSS custom properties once, then override just those variables inside the media query — every rule that uses var() updates automatically.
:root {
--bg-color: #ffffff;
--text-color: #111827;
--card-bg: #f9fafb;
--border-color: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #0f172a;
--text-color: #e2e8f0;
--card-bg: #1e293b;
--border-color: #334155;
}
}
body {
background: var(--bg-color);
color: var(--text-color);
}
.card {
background: var(--card-bg);
border: 1px solid var(--border-color);
}
Now adding dark-mode support to a brand-new component is free — as long as it's built using the shared variables, it already supports both themes with zero extra code.
Letting Visitors Override the System Preference
prefers-color-scheme alone can't be manually toggled by a button, since it reflects the OS setting. Sites that offer a manual light/dark switch typically combine this media query with a small bit of JavaScript that adds a class like .theme-dark to the <html> element and stores the choice in localStorage, then give that class selector higher priority than the media query:
/* System preference as the default... */
@media (prefers-color-scheme: dark) {
:root { --bg-color: #0f172a; }
}
/* ...but an explicit user choice always wins */
html.theme-dark {
--bg-color: #0f172a;
}
html.theme-light {
--bg-color: #ffffff;
}
This gives visitors a sensible dark/light default based on their device, while still letting anyone who prefers the opposite theme override it with one click — the best of both approaches.
