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

Why Every Website Needs a Dark Mode Toggle in 2026

Dark mode is no longer a nice-to-have feature. Users expect it. Operating systems, browsers, and apps all ship with dark themes, and visitors will leave your site if it blasts bright white light at them at midnight.

In this tutorial you will learn how to add a dark mode toggle to a website using plain CSS custom properties and a small JavaScript function. No frameworks, no libraries, no dependencies. The technique works on any website, whether it runs on Express.js, WordPress, a static site generator, or anything else.

Here is what we will cover:

  1. Planning a color scheme with CSS custom properties
  2. Building the toggle button in HTML
  3. Writing the JavaScript to switch themes
  4. Saving user preference in localStorage
  5. Respecting the prefers-color-scheme media query
  6. Avoiding common pitfalls

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

dark mode toggle button website

Step 1: Plan Your Color Scheme With CSS Custom Properties

The foundation of a maintainable dark mode is CSS custom properties (also called CSS variables). Instead of hard-coding color values throughout your stylesheet, you define them once on the :root selector and reference them everywhere else.

Define Light Mode Colors

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-heading: #000000;
  --color-primary: #2563eb;
  --color-surface: #f3f4f6;
  --color-border: #d1d5db;
}

Define Dark Mode Colors

We scope the dark palette to a .dark class on the <html> element. When that class is present, every variable is overridden automatically.

html.dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-heading: #f8fafc;
  --color-primary: #60a5fa;
  --color-surface: #1e293b;
  --color-border: #334155;
}

Use the Variables in Your Styles

body {
  background-color: var(--color-bg);
  color: var(--color-text);
  transition: background-color 0.3s ease, color 0.3s ease;
}

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

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

.card {
  background-color: var(--color-surface);
  border: 1px solid var(--color-border);
}

The transition property on the body gives users a smooth fade between themes instead of an abrupt flash.

Quick Color Palette Reference

Variable Light Value Dark Value Purpose
–color-bg #ffffff #0f172a Page background
–color-text #1a1a1a #e2e8f0 Body text
–color-heading #000000 #f8fafc Headings
–color-primary #2563eb #60a5fa Links, accents
–color-surface #f3f4f6 #1e293b Cards, panels
–color-border #d1d5db #334155 Borders, dividers

Step 2: Build the Toggle Button in HTML

Keep the markup simple. A single <button> element with an accessible aria-label is all you need.

<button
  id="theme-toggle"
  aria-label="Toggle dark mode"
  title="Toggle dark mode"
>
  <span class="icon-sun">☀</span>
  <span class="icon-moon">☾</span>
</button>

We show the sun icon when dark mode is active (meaning “click to switch to light”) and the moon icon when light mode is active.

Basic Toggle Button CSS

#theme-toggle {
  background: none;
  border: 2px solid var(--color-border);
  border-radius: 8px;
  padding: 6px 10px;
  cursor: pointer;
  font-size: 1.2rem;
  color: var(--color-text);
}

/* In light mode, hide the sun icon */
.icon-sun {
  display: none;
}

/* In dark mode, hide the moon icon and show the sun */
html.dark .icon-moon {
  display: none;
}

html.dark .icon-sun {
  display: inline;
}
dark mode toggle button website

Step 3: Write the JavaScript to Switch Themes

The JavaScript for a dark mode toggle is surprisingly short. All it does is add or remove the .dark class from the <html> element.

const toggle = document.getElementById('theme-toggle');

toggle.addEventListener('click', () => {
  document.documentElement.classList.toggle('dark');
});

That is a working toggle in three lines. But we still need to remember the user’s choice and respect their system preference. Let’s do that next.

Step 4: Save User Preference in localStorage

Without persistence, the theme resets on every page load. We use localStorage to remember the visitor’s choice across sessions.

const toggle = document.getElementById('theme-toggle');

toggle.addEventListener('click', () => {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

Now we need to read that value when the page loads and apply it before the page renders. This is important because if you apply the class too late, the user will see a flash of the wrong theme.

Inline Script in the <head>

Place this script inside the <head> tag, before your stylesheets finish loading. Because it is synchronous and tiny, it blocks rendering for only a fraction of a millisecond and prevents the dreaded flash of incorrect theme (FOIT).

<script>
  (function () {
    const saved = localStorage.getItem('theme');
    if (saved === 'dark') {
      document.documentElement.classList.add('dark');
    }
  })();
</script>

Step 5: Respect the prefers-color-scheme Media Query

Many users set a system-wide dark or light preference in their OS settings. A polite website should honor that preference when no explicit choice has been saved.

We update the inline head script to check for the media query as a fallback:

<script>
  (function () {
    const saved = localStorage.getItem('theme');
    if (saved) {
      if (saved === 'dark') {
        document.documentElement.classList.add('dark');
      }
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.classList.add('dark');
    }
  })();
</script>

The priority order is:

  1. Explicit user choice stored in localStorage (highest priority)
  2. System preference via prefers-color-scheme
  3. Light mode as the default fallback

Optional: Listen for System Changes in Real Time

If the user changes their OS theme while your page is open, you can react to it:

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    if (!localStorage.getItem('theme')) {
      document.documentElement.classList.toggle('dark', e.matches);
    }
  });

This listener only fires when the user has not explicitly chosen a theme on your site.

dark mode toggle button website

Step 6: The Complete Code

Here is every piece assembled into a single, copy-paste-ready example.

HTML

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Dark Mode Toggle Demo</title>

  <!-- Apply saved or system theme instantly -->
  <script>
    (function () {
      var saved = localStorage.getItem('theme');
      if (saved === 'dark') {
        document.documentElement.classList.add('dark');
      } else if (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches) {
        document.documentElement.classList.add('dark');
      }
    })();
  </script>

  <link rel="stylesheet" href="style.css" />
</head>
<body>
  <header>
    <h1>My Website</h1>
    <button id="theme-toggle" aria-label="Toggle dark mode">
      <span class="icon-sun">&#9728;</span>
      <span class="icon-moon">&#9790;</span>
    </button>
  </header>
  <main>
    <p>This page supports dark mode. Click the button to switch.</p>
  </main>

  <script src="theme-toggle.js"></script>
</body>
</html>

CSS (style.css)

:root {
  --color-bg: #ffffff;
  --color-text: #1a1a1a;
  --color-heading: #000000;
  --color-primary: #2563eb;
  --color-surface: #f3f4f6;
  --color-border: #d1d5db;
}

html.dark {
  --color-bg: #0f172a;
  --color-text: #e2e8f0;
  --color-heading: #f8fafc;
  --color-primary: #60a5fa;
  --color-surface: #1e293b;
  --color-border: #334155;
}

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

h1 {
  color: var(--color-heading);
}

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

header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 1rem 2rem;
  border-bottom: 1px solid var(--color-border);
}

#theme-toggle {
  background: none;
  border: 2px solid var(--color-border);
  border-radius: 8px;
  padding: 6px 12px;
  cursor: pointer;
  font-size: 1.25rem;
  color: var(--color-text);
}

.icon-sun { display: none; }
html.dark .icon-moon { display: none; }
html.dark .icon-sun { display: inline; }

JavaScript (theme-toggle.js)

const toggle = document.getElementById('theme-toggle');

toggle.addEventListener('click', () => {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

// React to OS-level theme changes when user has no saved preference
window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    if (!localStorage.getItem('theme')) {
      document.documentElement.classList.toggle('dark', e.matches);
    }
  });

Common Pitfalls to Avoid

After helping hundreds of developers add dark mode to Express.js-powered sites and other projects, we see the same mistakes again and again. Here is how to dodge them.

  • Flash of wrong theme. If you apply the class with JavaScript at the bottom of the page, the user sees the default theme first. Always use an inline script in the <head>.
  • Hardcoded colors in component styles. One rogue color: #333 in a component file will break your dark mode. Search your entire codebase for hardcoded color values and replace them with custom properties.
  • Forgetting images and SVGs. Logos and illustrations designed for light backgrounds can look terrible on dark surfaces. Consider using the <picture> element with a media="(prefers-color-scheme: dark)" source, or apply CSS filters to invert or adjust brightness.
  • Ignoring contrast ratios. Dark mode does not mean “make everything gray.” Run your dark palette through a contrast checker (WCAG AA requires at least 4.5:1 for body text) to make sure text remains readable.
  • Not testing form elements. Browser-default inputs, selects, and textareas often have their own background color. Use the color-scheme CSS property to tell the browser which scheme you are using: html.dark { color-scheme: dark; }

Bonus: Adding a Third “System” Option

Some sites offer three choices: Light, Dark, and System (auto). This gives users full control while still respecting OS preferences by default.

The logic changes slightly. Instead of storing dark or light, you store dark, light, or system.

function applyTheme(preference) {
  const isDark =
    preference === 'dark' ||
    (preference === 'system' &&
      window.matchMedia('(prefers-color-scheme: dark)').matches);

  document.documentElement.classList.toggle('dark', isDark);
}

// On page load
const saved = localStorage.getItem('theme') || 'system';
applyTheme(saved);

// When the user clicks a toggle
function setTheme(preference) {
  localStorage.setItem('theme', preference);
  applyTheme(preference);
}

You can wire this up to three radio buttons or a dropdown select instead of a single toggle button.

dark mode toggle button website

Using Dark Mode With Express.js Server-Side Rendering

If you render HTML on the server with Express.js (using EJS, Pug, Handlebars, or any template engine), you cannot read localStorage on the server. The best approach is:

  1. Always send the HTML without the .dark class.
  2. Include the tiny inline <head> script shown above so the class is applied before the page paints.
  3. Optionally, store the preference in a cookie so the server can read it and add the class during rendering. This fully eliminates the flash but adds more complexity.

For most projects the inline script approach is more than sufficient and keeps your server code clean.

Accessibility Checklist for Your Dark Mode Toggle

Requirement How to Implement
Keyboard accessible Use a <button> element (not a <div>)
Screen reader label Add aria-label="Toggle dark mode"
Visible focus ring Do not remove outline on focus; style it with :focus-visible
Sufficient contrast Verify all text meets WCAG AA contrast in both themes
Motion preference Wrap transitions in @media (prefers-reduced-motion: no-preference)

Frequently Asked Questions

How do I add a dark mode toggle to a website without JavaScript?

Pure CSS-only approaches exist using a hidden checkbox and the :checked pseudo-class to swap variables. However, you cannot save the user’s preference or detect the system theme without JavaScript, so the experience will reset on every page load. A small script is strongly recommended.

Does dark mode affect SEO?

No. Search engines like Google do not index visual styles. Dark mode has no direct impact on your rankings. However, a better user experience (less eye strain, longer session duration) can indirectly benefit engagement metrics.

Should I default to dark mode or light mode?

Default to whatever matches the user’s system preference using the prefers-color-scheme media query. If you cannot detect a preference, light mode is the safer default because most users are accustomed to it.

How do I handle images in dark mode?

You have several options:

  • Use the <picture> element with a dark-mode-specific source
  • Apply a subtle CSS brightness filter: html.dark img { filter: brightness(0.85); }
  • Use transparent PNGs or SVGs that adapt to the background color

Can I use this approach with Tailwind CSS?

Yes. Tailwind supports a darkMode: 'class' strategy that works exactly the same way. You add the dark class to the <html> element, and then use Tailwind’s dark: variant prefix in your markup (e.g., dark:bg-slate-900). The JavaScript toggle code in this tutorial is fully compatible with Tailwind’s class-based dark mode.

What if the user clears localStorage?

The toggle falls back to the system preference via the prefers-color-scheme media query. If neither is available, it defaults to light mode. The experience degrades gracefully.

Wrapping Up

Adding a dark mode toggle to your website takes less than 30 lines of CSS and JavaScript combined. The key ingredients are:

  • CSS custom properties for all your color values
  • A .dark class on the <html> element that overrides those properties
  • A tiny inline script in the <head> to apply the saved theme before paint
  • localStorage to remember the user’s choice
  • The prefers-color-scheme media query as an intelligent default

Start with the complete code example above, adapt the color values to match your brand, and your visitors will have a polished, accessible theme switcher in minutes.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.