← Back to DevBytes

How to Implement Dark Mode with CSS Custom Properties

What is Dark Mode with CSS Custom Properties?

Dark mode is a color scheme that uses light text and UI elements on a dark background. Implementing it with CSS custom properties (also called CSS variables) gives you a clean, maintainable way to switch themes without duplicating stylesheets or overriding dozens of rules. You define a palette of reusable color values once, then toggle their definitions when dark mode is active. The result is a smooth, scalable theme system that works across your entire stylesheet.

Why It Matters

🚀 Deploy your AI agent in 10 minutes

Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.

Try it free →

Building dark mode with custom properties offers several important advantages:

How to Implement Dark Mode Step by Step

1. Define Your Color Palette as Custom Properties

Start by defining a set of custom properties on the :root pseudo-class. These represent your light theme (the default). Use descriptive names like --bg, --text, --primary, etc., so you can reuse them consistently throughout your CSS.

:root {
  /* Light theme (default) */
  --bg: #ffffff;
  --bg-secondary: #f5f5f5;
  --text: #1a1a1a;
  --text-secondary: #555555;
  --primary: #0066cc;
  --border: #e0e0e0;
  --shadow: rgba(0, 0, 0, 0.1);
}

2. Create a Dark Theme Variant

Next, define a [data-theme="dark"] selector (or a class like .dark-mode) on the root element. Inside this rule, reassign the same custom properties to darker values. This is where the magic happens: you swap the palette without touching any component styles.

:root[data-theme="dark"] {
  /* Dark theme overrides */
  --bg: #121212;
  --bg-secondary: #1e1e1e;
  --text: #e0e0e0;
  --text-secondary: #a0a0a0;
  --primary: #3399ff;
  --border: #333333;
  --shadow: rgba(0, 0, 0, 0.5);
}

If you prefer a class-based approach, you can use:

:root.dark-mode {
  /* same dark values */
}

3. Apply the Custom Properties Throughout Your Styles

Use the custom properties everywhere you would normally hardcode colors. This ties your entire design to the variables, so a theme switch instantly updates everything.

body {
  background-color: var(--bg);
  color: var(--text);
  font-family: system-ui, sans-serif;
  transition: background-color 0.3s, color 0.3s;
}

.card {
  background-color: var(--bg-secondary);
  border: 1px solid var(--border);
  box-shadow: 0 2px 8px var(--shadow);
  padding: 1.5rem;
  border-radius: 0.75rem;
}

.card h2 {
  color: var(--text);
}

.card p {
  color: var(--text-secondary);
}

button {
  background-color: var(--primary);
  color: #ffffff;
  border: none;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  cursor: pointer;
}

4. Add a Theme Toggle with JavaScript

Create a button that switches the data-theme attribute on the <html> element. Store the user's choice in localStorage so it persists across page reloads.

<button id="theme-toggle">Toggle Dark Mode</button>

<script>
  const toggle = document.getElementById('theme-toggle');
  const root = document.documentElement;

  // Apply saved theme on load
  const savedTheme = localStorage.getItem('theme');
  if (savedTheme) {
    root.setAttribute('data-theme', savedTheme);
  }

  toggle.addEventListener('click', () => {
    const current = root.getAttribute('data-theme');
    const next = current === 'dark' ? 'light' : 'dark';
    root.setAttribute('data-theme', next);
    localStorage.setItem('theme', next);
  });
</script>

For a class-based toggle, you can use classList.toggle('dark-mode') and store a string flag accordingly.

5. Respect System Preferences with prefers-color-scheme

Many users configure a system-wide dark or light appearance. You can detect this with the prefers-color-scheme media query and set your default theme accordingly. Combine it with the saved user choice to create a seamless experience.

/* Default light theme (as above) */
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  /* ... */
}

/* Automatic dark if system prefers dark and no manual override */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --bg: #121212;
    --text: #e0e0e0;
    /* ... other dark values */
  }
}

In your JavaScript, adjust the logic to only save a theme when the user explicitly clicks the toggle. If no saved theme exists, let the media query decide.

// Check saved theme first; if none, respect system preference
const savedTheme = localStorage.getItem('theme');
if (savedTheme) {
  root.setAttribute('data-theme', savedTheme);
} else {
  // No manual override, media query handles it automatically
  // Optionally set data-theme based on matchMedia result
  const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
  root.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
}

// Toggle still works the same way
toggle.addEventListener('click', () => {
  const current = root.getAttribute('data-theme');
  const next = current === 'dark' ? 'light' : 'dark';
  root.setAttribute('data-theme', next);
  localStorage.setItem('theme', next);
});

Best Practices for Dark Mode with Custom Properties

Conclusion

Implementing dark mode with CSS custom properties turns a complex theming challenge into a straightforward, maintainable system. By defining a palette of variables on :root, overriding them with a data-theme selector, and sprinkling those variables throughout your styles, you give users a seamless way to switch between light and dark appearances. Add a simple JavaScript toggle and a prefers-color-scheme media query, and you've built an accessible, user-friendly experience that respects both explicit choices and system settings. This approach scales beautifully from small personal projects to large production applications.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles