How to Add a Dark Mode Toggle to Your Website with CSS and JavaScript

by | Jun 1, 2026 | Uncategorized | 0 comments

Why Every Website Needs a Dark Mode Toggle in 2026

Dark mode is no longer a nice-to-have feature. Users expect it. Whether they are browsing late at night, reducing eye strain, or simply preferring a darker interface, giving visitors control over their viewing experience is a clear win for usability and accessibility.

In this tutorial, you will learn how to build a fully functional dark mode toggle for your website using CSS custom properties (variables) and a small JavaScript snippet. We will also cover two critical extras that most tutorials skip:

  • Respecting the visitor’s operating system preference (via prefers-color-scheme)
  • Saving the user’s choice in localStorage so it persists across page loads

By the end you will have a lightweight, production-ready dark mode toggle you can drop into any project.

dark mode light mode toggle button

Before You Start: What You Need

Requirement Details
HTML knowledge Basic understanding of markup structure
CSS knowledge Familiarity with custom properties (CSS variables)
JavaScript knowledge Comfortable with DOM manipulation and events
Code editor VS Code, Sublime Text, or any editor you prefer

Step 1: Define Your Color Scheme with CSS Custom Properties

The foundation of a clean dark mode toggle is CSS custom properties. Instead of scattering color values across dozens of selectors, you define them once on the :root element and swap them when the theme changes.

Create or open your main CSS file and add the following:

/* Light theme (default) */
:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-heading: #000000;
  --color-surface: #f4f4f5;
  --color-border: #d1d5db;
  --color-accent: #2563eb;
}

/* Dark theme */
[data-theme="dark"] {
  --color-bg: #121212;
  --color-text: #e0e0e0;
  --color-heading: #ffffff;
  --color-surface: #1e1e1e;
  --color-border: #333333;
  --color-accent: #60a5fa;
}

The trick here is the [data-theme="dark"] attribute selector. When we add data-theme="dark" to the <html> element, every variable updates instantly and the entire page re-renders in dark mode. No extra classes needed on individual elements.

Apply the Variables to Your CSS

Now use these variables throughout your stylesheet:

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

h1, h2, h3, h4 {
  color: var(--color-heading);
}

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

.card {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
  border-radius: 8px;
  padding: 1.5rem;
}

The transition property on the body gives you a smooth fade between light and dark themes instead of a jarring flash.

Step 2: Create the Toggle Button in HTML

Keep the markup simple. A single button with an id and an accessible aria-label is all you need:

<button id="theme-toggle" aria-label="Toggle dark mode" title="Toggle dark mode">
  <span class="toggle-icon" aria-hidden="true">🌙</span>
</button>

We use a moon emoji as the default icon. The JavaScript will swap it to a sun when dark mode is active. You could also use SVG icons for a more polished look.

Style the Toggle Button

#theme-toggle {
  background: none;
  border: 2px solid var(--color-border);
  border-radius: 50%;
  width: 44px;
  height: 44px;
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 1.25rem;
  transition: border-color 0.3s ease;
}

#theme-toggle:hover {
  border-color: var(--color-accent);
}

#theme-toggle:focus-visible {
  outline: 2px solid var(--color-accent);
  outline-offset: 2px;
}

Notice the :focus-visible rule. This ensures keyboard users see a clear focus ring while mouse users do not get an unwanted outline. Accessibility matters.

dark mode light mode toggle button

Step 3: Write the JavaScript Toggle Logic

Here is the complete JavaScript. You can place it in a separate file or in a <script> tag right before the closing </body> tag:

(function () {
  const toggle = document.getElementById('theme-toggle');
  const icon = toggle.querySelector('.toggle-icon');
  const root = document.documentElement; // <html>

  // 1. Determine the initial theme
  function getPreferredTheme() {
    // Check localStorage first
    const stored = localStorage.getItem('theme');
    if (stored) return stored;

    // Fall back to OS preference
    return window.matchMedia('(prefers-color-scheme: dark)').matches
      ? 'dark'
      : 'light';
  }

  // 2. Apply theme to the document
  function applyTheme(theme) {
    root.setAttribute('data-theme', theme);
    icon.textContent = theme === 'dark' ? '\u2600\uFE0F' : '\uD83C\uDF19';
    toggle.setAttribute(
      'aria-label',
      theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'
    );
  }

  // 3. Initialize on page load
  applyTheme(getPreferredTheme());

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

  // 5. Listen for OS-level changes
  window
    .matchMedia('(prefers-color-scheme: dark)')
    .addEventListener('change', function (e) {
      if (!localStorage.getItem('theme')) {
        applyTheme(e.matches ? 'dark' : 'light');
      }
    });
})();

What This Script Does (Line by Line)

  1. getPreferredTheme() checks localStorage for a previously saved choice. If nothing is stored, it reads the user’s operating system preference via the prefers-color-scheme media query.
  2. applyTheme() sets the data-theme attribute on the <html> element, swaps the icon, and updates the aria-label for screen readers.
  3. On page load, we call applyTheme immediately so there is no flash of wrong theme.
  4. The click handler toggles between dark and light and writes the choice to localStorage.
  5. The change listener on the media query ensures that if the user changes their OS setting (for example, macOS auto-switching at sunset) and they have not manually picked a theme, the website follows along.

Step 4: Prevent the Flash of Incorrect Theme (FOIT)

One common problem: the page loads in light mode for a split second before JavaScript kicks in and switches to dark. This is called a Flash of Incorrect Theme. To fix it, add a tiny inline script in the <head> of your HTML, before any stylesheets:

<script>
  (function () {
    var theme = localStorage.getItem('theme');
    if (!theme) {
      theme = window.matchMedia('(prefers-color-scheme: dark)').matches
        ? 'dark'
        : 'light';
    }
    document.documentElement.setAttribute('data-theme', theme);
  })();
</script>

Because this runs synchronously before the browser paints the page, the correct theme is applied instantly. No flicker. This small detail separates a polished implementation from an amateur one.

Step 5: Respecting System Preferences the Right Way

Many dark mode toggle tutorials only use JavaScript to detect system preferences. But you can also use a pure CSS media query as a fallback for users who have JavaScript disabled:

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --color-bg: #121212;
    --color-text: #e0e0e0;
    --color-heading: #ffffff;
    --color-surface: #1e1e1e;
    --color-border: #333333;
    --color-accent: #60a5fa;
  }
}

The selector :root:not([data-theme="light"]) means: apply dark colors if the OS prefers dark and the user has not explicitly chosen light mode. This creates a graceful degradation layer.

Complete File Structure

Here is a quick overview of how the files fit together:

File Purpose
index.html Page structure, inline head script, toggle button
styles.css CSS custom properties, light/dark variables, button styles
theme-toggle.js Toggle logic, localStorage, OS preference listener
dark mode light mode toggle button

Bonus: Supporting Three States (Light, Dark, Auto)

Some implementations offer three options: Light, Dark, and Auto (system default). The SERP result from clagnut.com mentions this approach using radio buttons. Here is how you can adapt the logic:

  1. Replace the single button with three options (radio buttons or a dropdown).
  2. Store one of three values in localStorage: light, dark, or auto.
  3. When the value is auto, remove the data-theme attribute and let the CSS prefers-color-scheme media query handle everything.

This gives power users full control while keeping the default behavior automatic.

Accessibility Checklist for Your Dark Mode Toggle

Before shipping, run through this quick checklist:

  • Contrast ratios: Verify that both themes meet WCAG AA (4.5:1 for body text). Use a tool like the WebAIM Contrast Checker.
  • Keyboard navigation: The toggle button must be focusable and operable with Enter or Space.
  • Screen reader labels: Update aria-label dynamically so screen readers announce the current action (“Switch to dark mode” or “Switch to light mode”).
  • Reduced motion: If you use transitions, wrap them in a @media (prefers-reduced-motion: no-preference) query so users who are sensitive to motion are not affected.

Common Mistakes to Avoid

After reviewing dozens of dark mode toggle implementations, here are the pitfalls we see most often:

  • Hardcoding colors instead of using CSS variables. This makes maintenance painful and toggling nearly impossible without duplicating entire stylesheets.
  • Forgetting images and media. Some images look terrible on a dark background. Consider using the <picture> element with a media attribute or applying CSS filters to invert/dim images when dark mode is active.
  • Not testing third-party embeds. Embedded widgets (maps, forms, videos) may not respond to your theme variables. Check them in both modes.
  • Ignoring the flash of incorrect theme. Always include the inline head script described in Step 4.
  • Overriding the user’s manual choice with OS changes. If a user explicitly picks light mode, do not switch them to dark just because their OS changed. The localStorage check in our script handles this correctly.
dark mode light mode toggle button

Performance Considerations

A dark mode toggle website feature should add virtually zero performance overhead. Our implementation is lightweight because:

  • No external libraries are required.
  • The JavaScript is under 1 KB unminified.
  • CSS custom properties are natively supported in all modern browsers (over 97% global support as of early 2026).
  • localStorage is synchronous and extremely fast for reading a single key.

If you are using a framework like React, Vue, or Svelte, you can wrap this same logic in a composable or hook. The core concept does not change.

Testing Your Dark Mode Toggle

Here is how to thoroughly test your implementation:

  1. Manual toggle: Click the button and verify that all colors, borders, and backgrounds switch correctly.
  2. Page reload: Choose dark mode, reload the page, and confirm the choice persists without any flash.
  3. New tab: Open a new tab to the same site and verify the saved theme is applied.
  4. Clear localStorage: Remove the theme key and reload. The site should follow the OS preference.
  5. Change OS setting: Toggle your operating system between light and dark mode (with no localStorage value set) and confirm the site reacts in real time.
  6. Disable JavaScript: If you added the CSS-only fallback, verify that the OS preference still controls the theme.

Wrapping Up

Adding a dark mode toggle to your website is one of the highest-impact UX improvements you can make with a minimal amount of code. To recap the key steps:

  1. Define light and dark color schemes using CSS custom properties.
  2. Create a simple, accessible toggle button.
  3. Write a small JavaScript function that toggles a data-theme attribute on the <html> element.
  4. Store the user’s choice in localStorage.
  5. Respect system preferences with prefers-color-scheme.
  6. Prevent the flash of incorrect theme with an inline head script.

The entire implementation requires zero dependencies, weighs under 1 KB of JavaScript, and works in every modern browser. Copy the code snippets from this tutorial, adapt the color values to your brand, and ship it.

Frequently Asked Questions

Does adding a dark mode toggle affect SEO?

Not directly. Search engines do not rank pages differently based on theme support. However, improved user experience (lower bounce rates, longer session times) can indirectly benefit your SEO performance. A dark mode toggle website also signals modern design, which builds trust with visitors.

Can I use this approach with WordPress?

Yes. You can add the CSS to your theme’s stylesheet (or the WordPress Customizer’s “Additional CSS” section), place the inline script in your theme’s header.php, and enqueue the main JavaScript file normally. Many WordPress themes in 2026 also support dark mode natively through the theme settings.

What about CSS frameworks like Tailwind CSS or Bootstrap?

Tailwind CSS has built-in dark mode support using the dark: variant. You can configure it to use a class or data-attribute strategy, which works perfectly with the JavaScript toggle described here. Bootstrap does not have native dark mode toggling out of the box, but you can override its CSS variables with the same [data-theme="dark"] approach.

Should I default to light or dark mode?

The best practice is to default to the user’s operating system preference using the prefers-color-scheme media query. If no preference is detected, light mode is the safer default because it matches what most users expect from the web.

How do I handle images in dark mode?

You have several options. You can use the <picture> element with a media="(prefers-color-scheme: dark)" attribute to serve different images. Alternatively, you can apply a subtle CSS filter like filter: brightness(0.85) to dim images in dark mode. For logos, consider using transparent PNGs or SVGs that adapt to the background color.

Is localStorage the best way to store the theme preference?

localStorage is ideal for this use case. It is synchronous (no flash on page load), persists across sessions, and requires no server-side setup. Cookies are an alternative if you need the preference on the server (for example, to server-render the correct theme), but for a purely client-side toggle, localStorage is simpler and more performant.

Search Keywords

Recent Posts

Subscribe Now!