Adding Dark Mode with CSS Variables and a JavaScript Toggle

Table of Contents
Big thanks to our contributors those make our blogs possible.

Our growing community of contributors bring their unique insights from around the world to power our blog. 

Introduction

Dark mode has become a must-have feature for modern websites and applications, offering users a comfortable, low-light viewing experience that reduces eye strain and conserves battery life on OLED screens. Implementing dark mode using CSS variables and a simple JavaScript toggle is both elegant and maintainable: by defining your colors as variables, you can switch themes by updating just a few values. In this guide, you’ll learn how to structure your CSS variables, apply them to your UI components, and write a lightweight JavaScript toggle—plus, persist the user’s preference across sessions with localStorage. Let’s dive in!

1. Setting Up Your CSS Variables

CSS variables (custom properties) let you define reusable values that can be dynamically overridden.

cssCopyEdit:root {
  /* Light theme colors */
  --bg-color: #ffffff;
  --text-color: #333333;
  --link-color: #0066cc;
  --card-bg: #f9f9f9;
  --border-color: #dddddd;
}

[data-theme="dark"] {
  /* Dark theme overrides */
  --bg-color: #121212;
  --text-color: #eeeeee;
  --link-color: #66aaff;
  --card-bg: #1e1e1e;
  --border-color: #333333;
}

/* Apply variables throughout your stylesheet */
body {
  background-color: var(--bg-color);
  color: var(--text-color);
  transition: background-color 0.3s ease, color 0.3s ease;
}

a {
  color: var(--link-color);
}

.card {
  background-color: var(--card-bg);
  border: 1px solid var(--border-color);
  border-radius: 4px;
  padding: 1rem;
  transition: background-color 0.3s ease, border-color 0.3s ease;
}

Why This Works

  • :root defines default (light) theme variables.
  • [data-theme="dark"] overrides just those variables you want to change for dark mode.
  • UI components reference variables via var(--variable-name), so switching themes is instantaneous.

2. Creating the Toggle Button

Add a simple HTML button to your layout—ideally in a consistent location, like the header:

htmlCopyEdit<header>
  <button id="theme-toggle" aria-label="Toggle dark mode">🌓</button>
</header>
  • The emoji provides a quick visual cue; you can swap in an icon or text as desired.
  • aria-label ensures accessibility for screen-reader users.

3. Writing the JavaScript Toggle Logic

Use JavaScript to switch the data-theme attribute on the <html> (or <body>) element and store the user’s choice in localStorage.

javascriptCopyEdit(() => {
  const toggle = document.getElementById('theme-toggle');
  const root = document.documentElement;
  const storedTheme = localStorage.getItem('theme');

  // Initialize theme on page load
  if (storedTheme) {
    root.setAttribute('data-theme', storedTheme);
  }

  // Determine default (system) preference if no choice stored
  else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
    root.setAttribute('data-theme', 'dark');
  }

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

How It Works

  1. Load Preference: On page load, check localStorage; if no stored theme, fall back to the user’s OS-level preference via prefers-color-scheme.
  2. Toggle: Flip between "dark" and "light", update the data-theme attribute, and save the new choice.
  3. Persistent: Because you save to localStorage, the site remembers the user’s selection on subsequent visits.

4. Enhancements and Best Practices

  • Smooth Transition: Add transition rules (as shown) so colors fade rather than jump.
  • Accessibility: Ensure contrast ratios meet WCAG guidelines in both themes.
  • Initial Flash Prevention: To avoid a flash of the wrong theme on load, you can inline a small script in your <head> that reads localStorage and sets the attribute before CSS loads: htmlCopyEdit<script> (function() { const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); document.documentElement.setAttribute('data-theme', theme); })(); </script>
  • Icon Swap: Swap the toggle’s icon or text based on the active theme for user clarity: javascriptCopyEditfunction updateToggleIcon(theme) { toggle.textContent = theme === 'dark' ? '☀️' : '🌙'; } // Call updateToggleIcon when initializing and toggling
  • Modular CSS: If using a CSS preprocessor or component-based framework, you can import your variables centrally and reference them in component styles.

Conclusion

By leveraging CSS variables for theming and a few lines of JavaScript for toggling and persistence, you can deliver a seamless dark mode experience with minimal overhead. This approach scales elegantly as your design system grows—just update variable definitions, and the entire UI responds. Try integrating this pattern into your next project, ensuring you test for contrast, performance, and accessibility to delight users who prefer or require dark mode.

Let's connect on TikTok

Join our newsletter to stay updated

Sydney Based Software Solutions Professional who is crafting exceptional systems and applications to solve a diverse range of problems for the past 10 years.

Share the Post

Related Posts