Advanced20 min read

Dark Mode Theming

Learn how to implement dark and light themes using CSS custom properties, the prefers-color-scheme media query, and the data-theme attribute pattern.

Why Dark Mode?

Dark mode has become a standard feature of modern websites and applications. It offers several benefits:

  • Reduced eye strain in low-light environments.
  • Lower battery consumption on OLED screens (black pixels are turned off).
  • User preference — many people simply prefer a darker interface.

There are two main approaches to implementing dark mode:

  1. System preference detection — Use the prefers-color-scheme media query to automatically match the user's OS setting.
  2. Manual toggle — Use a 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.

Theme with prefers-color-scheme

html
<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 data-theme Attribute Approach

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:

css
/* 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:

javascript
// 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);

Combining both approaches

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.

Transition tips

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?

Ready to practice?

Create your free account to access the interactive code editor, run challenges, and track your progress.