Learn how to implement dark and light themes using CSS custom properties, the prefers-color-scheme media query, and the data-theme attribute pattern.
Dark mode has become a standard feature of modern websites and applications. It offers several benefits:
There are two main approaches to implementing dark mode:
prefers-color-scheme media query to automatically match the user's OS setting.data-theme attribute (or a class) on the <html> element, toggled by JavaScript, to let users choose.Both approaches rely on CSS custom properties to define color values that change between themes. Instead of hardcoding colors everywhere, you define them once as variables and swap the values for each theme.
<style>
/* Light theme (default) */
:root {
--bg-color: #ffffff;
--text-color: #1a1a1a;
--card-bg: #f5f5f5;
--border-color: #ddd;
--primary: #3498db;
}
/* Dark theme — activated by OS preference */
@media (prefers-color-scheme: dark) {
:root {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
--card-bg: #2d2d2d;
--border-color: #444;
--primary: #5dade2;
}
}
body {
background-color: var(--bg-color);
color: var(--text-color);
font-family: sans-serif;
padding: 24px;
transition: background-color 0.3s, color 0.3s;
}
.card {
background-color: var(--card-bg);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 16px;
}
.card h3 {
color: var(--primary);
}
</style>
<div class="card">
<h3>Auto Dark Mode</h3>
<p>This card adapts to your system color scheme preference.</p>
</div>The prefers-color-scheme approach is automatic but does not let users override their system setting. A more flexible pattern uses a data-theme attribute on the root element:
/* Light theme (default) */
:root,
[data-theme="light"] {
--bg-color: #ffffff;
--text-color: #1a1a1a;
}
/* Dark theme */
[data-theme="dark"] {
--bg-color: #1a1a1a;
--text-color: #e0e0e0;
}Then JavaScript toggles the attribute:
// Toggle dark mode
const html = document.documentElement;
html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark';
// Persist the choice in localStorage
localStorage.setItem('theme', html.dataset.theme);The best implementations respect the system preference by default but allow users to override it. On page load, check localStorage first; if no saved preference exists, fall back to prefers-color-scheme.
Add transition: background-color 0.3s, color 0.3s on the body for a smooth theme switch. Avoid transition: all as it can cause performance issues by animating every property.
What does the `prefers-color-scheme: dark` media query detect?