`prefers-color-scheme` and a Token-Based Approach
Dark Mode
Respect the user's system preference with `prefers-color-scheme`, plus an optional manual toggle.
What you'll learn
- Respect the user's OS preference
- Build a `data-theme` toggle
- Use `color-scheme` for native form controls
A modern site supports both light and dark themes. Two pieces — respect the user’s OS setting, and offer a manual toggle.
OS Preference
:root {
--bg: white;
--text: #111;
--brand: #6360ff;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #111;
--text: #f4f4f5;
--brand: #918eff;
}
}
body {
background: var(--bg);
color: var(--text);
} When the OS is in dark mode, the variables flip. Everything that uses them re-themes.
Manual Toggle
For a user-controlled toggle, attach the theme to the document via
a data-theme attribute:
:root {
--bg: white;
--text: #111;
}
:root[data-theme="dark"] {
--bg: #111;
--text: #f4f4f5;
} JS sets the attribute:
document.documentElement.dataset.theme = "dark"; To honor BOTH OS preference and a manual override:
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--bg: #111;
--text: #f4f4f5;
}
}
:root[data-theme="dark"] {
--bg: #111;
--text: #f4f4f5;
} color-scheme — Native Controls
:root { color-scheme: light dark; } Tells the browser the page supports both — native scrollbars, form controls, and selection colors adapt.
For just dark:
[data-theme="dark"] {
color-scheme: dark;
} Avoid the Flash
A naïve toggle reads JS BEFORE applying — meaning the page flashes
in light mode before going dark. Move the theme decision to a small
inline <script> at the top of <head>, BEFORE the CSS loads:
<script>
const t = localStorage.getItem("theme") ||
(matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light");
document.documentElement.dataset.theme = t;
</script> Up Next
Scroll snap for carousels and slide decks.
Scroll Snap →