search
HomeWeb Front-endCSS TutorialBuild a Dark Mode Toggle in inutes (That Actually Works)

Build a Dark Mode Toggle in inutes (That Actually Works)

Ever spent hours implementing a dark mode toggle only to have it flash blindingly white on page refresh? Or worse, have it completely ignore your user's system preferences? Yeah, me too. ?

Here's the thing - dark mode isn't just a trendy feature anymore. With more people coding at night (guilty as charged) and accessibility becoming increasingly important, a well-implemented dark mode is practically a must-have for modern websites or web apps. But getting it right can be surprisingly tricky.

The good news? After fumbling through various implementations and fighting with localStorage, I've finally cracked the code for a dark mode toggle that:

  • Actually remembers your user's preference
  • Doesn't flash the wrong theme on reload
  • Plays nicely with system preferences
  • Takes literally 5 minutes to implement

In this post, I'll walk you through building a dark mode toggle that you'll actually want to use. No overcomplicated solutions, no unnecessary dependencies - just clean, working code that you can implement right away.

Prerequisites (The Stuff You'll Need)

Let's get the boring part out of the way first - but I promise to keep it short!

You probably already have everything you need, but just to make sure we're on the same page:

  • Basic HTML (like, you know what a
  • Some CSS knowledge (especially CSS variables - but I'll explain as we go)
  • Vanilla JavaScript (nothing fancy, I promise)
  • Your favorite code editor
  • About 5 minutes of your time (and maybe a coffee ☕)

What We're Building

Before we jump in, here's a quick peek at what we'll end up with. No fancy UI libraries or complicated setups - just a simple, smooth toggle that looks something like this:

Don't worry about making it look exactly like this - the important part is that it'll work flawlessly. We'll focus on function first, then you can style it however you want.

The best part? Everything we're about to build works with:

  • Modern browsers (yes, even Safari!)
  • System dark mode preferences
  • Page refreshes (no more flash of white screen)
  • Zero external dependencies

Ready to get your hands dirty? Let's start with the foundation!

Setting Up the Foundation

Alright, let's get our hands dirty! First, we'll set up the basic structure.

The HTML: Keep It Simple

We'll start with some dead-simple HTML. No need to overthink this part:

<button>



<h3>
  
  
  The CSS
</h3>

<p>Here's where things get interesting. We'll use CSS variables (aka custom properties) to handle our color scheme. Drop this in your CSS file:<br>
</p>

<pre class="brush:php;toolbar:false">:root {
  --background: #ffffff;
  --text-primary: #222222;
  --toggle-bg: #e4e4e7;
  --toggle-hover: #d4d4d8;
}


[data-theme="dark"] {
  --background: #121212;
  --text-primary: #ffffff;
  --toggle-bg: #3f3f46;
  --toggle-hover: #52525b;
}

body {
  background-color: var(--background);
  color: var(--text-primary);
  transition: background-color 0.3s ease, color 0.3s ease;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.theme-toggle {
  border: none;
  padding: 0.5rem;
  border-radius: 9999px;
  background-color: var(--toggle-bg);
  cursor: pointer;
  transition: background-color 0.2s ease;
  align-self: flex-start;
  position: absolute;
  right: 20px;
}

.theme-toggle:hover {
  background-color: var(--toggle-hover);
}

.theme-toggle svg {
    transform-origin: center;
    transition: transform 0.3s ease;
}

.theme-toggle:active svg {
    transform: rotate(30deg);
}

h1 {
  display: flex;
}

.sun-icon {
  display: none;
  width: 24px;
  height: 24px;
}

.moon-icon {
  width: 24px;
  height: 24px;
}

[data-theme="dark"] .sun-icon {
  display: block;
}

[data-theme="dark"] .moon-icon {
  display: none;
}

Pro tip: Notice how we're using data-theme instead of classes? This makes it super clear what the attribute is for and prevents any potential class naming conflicts.

For the icons, you can use either your own SVGs or grab some from your favorite icon library. I like using simple ones so I told Chatgpt to come up with this:

<!-- Replace the empty SVGs with these -->
<svg>



<p>At this point, your toggle should look pretty decent, but it won't actually do anything yet. Don't worry though - in the next section, we'll add the JavaScript that makes it all work!</p>

<p><strong>A quick heads-up:</strong> I've kept the styling minimal on purpose. Feel free to spice it up with your own creative touches. Want a sliding animation? Go for it! Prefer a different icon style? Make it yours!</p>

<p>Ready to make this thing actually work? Let's move on to the JavaScript implementation!</p>

<h2>
  
  
  The JavaScript Implementation (Where It All Comes Together!)
</h2>

<p>Alright, this is where we make our toggle actually, you know... toggle. But don't worry - we're keeping it clean and simple.<br>
</p>

<pre class="brush:php;toolbar:false">const themeToggle = document.querySelector('.theme-toggle');


function toggleTheme() {
  const currentTheme = document.documentElement.getAttribute('data-theme');

  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';

  document.documentElement.setAttribute('data-theme', newTheme);

  localStorage.setItem('theme', newTheme);
}

// Listen for clicks on our toggle
themeToggle.addEventListener('click', toggleTheme);


function initializeTheme() {
  const savedTheme = localStorage.getItem('theme');

  if (savedTheme) {
    document.documentElement.setAttribute('data-theme', savedTheme);
  } else {
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

    document.documentElement.setAttribute(
      'data-theme',
      prefersDark ? 'dark' : 'light'
    );

    localStorage.setItem('theme', prefersDark ? 'dark' : 'light');
  }
}

// Run on page load
initializeTheme();

// Listen for system theme change

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    // Only update if user hasn't manually set a preference
    if (!localStorage.getItem('theme')) {
      document.documentElement.setAttribute(
        'data-theme',
        e.matches ? 'dark' : 'light'
      );
    }
  });

Let's break down what's happening here because there's actually some pretty cool stuff going on:

  1. When someone clicks the toggle, we check the current theme and switch to the opposite one
  2. We save their choice in localStorage (so it persists between page loads)
  3. When the page first loads, we:
    • Check if they have a saved preference
    • If not, we check their system theme
    • Apply whichever theme is appropriate
  4. As a bonus, we listen for system theme changes (like when someone enables dark mode on their OS)

Preventing the Flash of Wrong Theme

Here's a common problem: sometimes users see a flash of the wrong theme when the page loads. Super annoying, right? Let's fix that by adding this script in the

of your HTML:
<script>
  // Add this to your <head> before any style sheets
  (function() {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      document.documentElement.setAttribute('data-theme', savedTheme);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>

This runs immediately before anything else loads, preventing that annoying flash.

And... that's it! You've got a working dark mode toggle that:

  • Remembers user preferences
  • Respects system settings
  • Doesn't flash the wrong theme
  • Works smoothly with transitions

Want to make it even better? Let's move on to some helpful tips that'll take your toggle from good to great!

Making it Better (Because Details Matter!)

Let's take our toggle from "it works" to "it works beautifully" with some important but often overlooked improvements. These are the kinds of details that separate a professional implementation from a quick hack.

1. Keyboard Accessibility

First up, let's make sure everyone can use our toggle, regardless of how they interact with their device:

// Add this to your existing JavaScript
themeToggle.addEventListener('keydown', (e) => {
    // Toggle on Enter or Space
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        toggleTheme();
    }
});

2. Disabling transition

Disable transitions when user prefers reduced motion:

@media (prefers-reduced-motion: reduce) {
  body {
    transition: none;
  }
}

3. Handling Content Changes

Here's something many developers miss - some content might need to change based on the theme. Think about images with different versions for light/dark modes:

// Add this to your toggleTheme function
function updateThemeSpecificContent(theme) {
    // Find all theme-aware images
    const themeImages = document.querySelectorAll('[data-theme-image]');

    themeImages.forEach(img => {
        const lightSrc = img.getAttribute('data-light-src');
        const darkSrc = img.getAttribute('data-dark-src');

        img.src = theme === 'dark' ? darkSrc : lightSrc;
    });
}

Use it in your HTML like this:

<img src="/static/imghwm/default1.png" data-src="/path/to/light-logo.png" class="lazy" data-theme-image data-light- data-dark- alt="Build a Dark Mode Toggle in inutes (That Actually Works)">

4. Preventing Theme Mismatch

Sometimes the saved theme can get out of sync with what's actually showing. Let's add a safety check:

<button>



<h3>
  
  
  The CSS
</h3>

<p>Here's where things get interesting. We'll use CSS variables (aka custom properties) to handle our color scheme. Drop this in your CSS file:<br>
</p>

<pre class="brush:php;toolbar:false">:root {
  --background: #ffffff;
  --text-primary: #222222;
  --toggle-bg: #e4e4e7;
  --toggle-hover: #d4d4d8;
}


[data-theme="dark"] {
  --background: #121212;
  --text-primary: #ffffff;
  --toggle-bg: #3f3f46;
  --toggle-hover: #52525b;
}

body {
  background-color: var(--background);
  color: var(--text-primary);
  transition: background-color 0.3s ease, color 0.3s ease;
  height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
}

.theme-toggle {
  border: none;
  padding: 0.5rem;
  border-radius: 9999px;
  background-color: var(--toggle-bg);
  cursor: pointer;
  transition: background-color 0.2s ease;
  align-self: flex-start;
  position: absolute;
  right: 20px;
}

.theme-toggle:hover {
  background-color: var(--toggle-hover);
}

.theme-toggle svg {
    transform-origin: center;
    transition: transform 0.3s ease;
}

.theme-toggle:active svg {
    transform: rotate(30deg);
}

h1 {
  display: flex;
}

.sun-icon {
  display: none;
  width: 24px;
  height: 24px;
}

.moon-icon {
  width: 24px;
  height: 24px;
}

[data-theme="dark"] .sun-icon {
  display: block;
}

[data-theme="dark"] .moon-icon {
  display: none;
}

5. Performance Optimization Trick

Here's a neat trick to prevent layout shifts when loading custom fonts in different themes:

<!-- Replace the empty SVGs with these -->
<svg>



<p>At this point, your toggle should look pretty decent, but it won't actually do anything yet. Don't worry though - in the next section, we'll add the JavaScript that makes it all work!</p>

<p><strong>A quick heads-up:</strong> I've kept the styling minimal on purpose. Feel free to spice it up with your own creative touches. Want a sliding animation? Go for it! Prefer a different icon style? Make it yours!</p>

<p>Ready to make this thing actually work? Let's move on to the JavaScript implementation!</p>

<h2>
  
  
  The JavaScript Implementation (Where It All Comes Together!)
</h2>

<p>Alright, this is where we make our toggle actually, you know... toggle. But don't worry - we're keeping it clean and simple.<br>
</p>

<pre class="brush:php;toolbar:false">const themeToggle = document.querySelector('.theme-toggle');


function toggleTheme() {
  const currentTheme = document.documentElement.getAttribute('data-theme');

  const newTheme = currentTheme === 'dark' ? 'light' : 'dark';

  document.documentElement.setAttribute('data-theme', newTheme);

  localStorage.setItem('theme', newTheme);
}

// Listen for clicks on our toggle
themeToggle.addEventListener('click', toggleTheme);


function initializeTheme() {
  const savedTheme = localStorage.getItem('theme');

  if (savedTheme) {
    document.documentElement.setAttribute('data-theme', savedTheme);
  } else {
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

    document.documentElement.setAttribute(
      'data-theme',
      prefersDark ? 'dark' : 'light'
    );

    localStorage.setItem('theme', prefersDark ? 'dark' : 'light');
  }
}

// Run on page load
initializeTheme();

// Listen for system theme change

window.matchMedia('(prefers-color-scheme: dark)')
  .addEventListener('change', (e) => {
    // Only update if user hasn't manually set a preference
    if (!localStorage.getItem('theme')) {
      document.documentElement.setAttribute(
        'data-theme',
        e.matches ? 'dark' : 'light'
      );
    }
  });

Quick Testing Checklist

Before you ship, make sure to test these scenarios:

  • ✅ Page refresh maintains the correct theme
  • ✅ System theme changes are respected (if no manual preference)
  • ✅ Toggle works with both mouse and keyboard
  • ✅ No flashing of wrong theme on load
  • ✅ Transitions are smooth
  • ✅ Works in all major browsers (yes, even Safari!)
  • ✅ Theme-specific content updates correctly

Common Issues to Watch Out For

  1. Third-party Content: Some embedded content might not respect your theme. Handle it like this:
<script>
  // Add this to your <head> before any style sheets
  (function() {
    const savedTheme = localStorage.getItem('theme');
    if (savedTheme) {
      document.documentElement.setAttribute('data-theme', savedTheme);
    } else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
      document.documentElement.setAttribute('data-theme', 'dark');
    }
  })();
</script>
  1. Images with Transparency: They can look wrong on different backgrounds:
// Add this to your existing JavaScript
themeToggle.addEventListener('keydown', (e) => {
    // Toggle on Enter or Space
    if (e.key === 'Enter' || e.key === ' ') {
        e.preventDefault();
        toggleTheme();
    }
});

That's it! You now have a robust, accessible, and user-friendly dark mode implementation that handles different scenarios like a champ.

Conclusion

Well, there you have it! We've built a dark mode toggle that not only works but works really well.

Let's quickly recap what we've accomplished:

  • A toggle that actually remembers your users' preferences ?
  • Smooth transitions between themes without the dreaded flash ⚡
  • System preference detection that just works ?
  • Accessibility built-in from the start ♿
  • Performance optimizations to keep things snappy ?‍♂️

Where to Go From Here?

Feel free to take this code and make it your own. Maybe add some fancy animations, try out different color schemes, or integrate it with your existing design. The foundation we've built is solid enough to handle whatever creative ideas you throw at it.

One Last Thing...

Dark mode might seem like a small detail, but it's these little touches that show you care about your users' experience. Plus, it's just cool. Who doesn't love a good dark mode?

If you found this helpful, feel free to share it with other developers. And if you come up with any cool improvements, I'd love to hear about them!


Let's Stay Connected! ?

If you enjoyed this guide and want more web dev tips, hacks, and occasional dad jokes about programming, come hang out with me on X! I share quick tips, coding insights, and real-world solutions from my developer journey.

? Follow me @Peboydcoder

I regularly post about:

  • Web development tips & tricks
  • Frontend best practices
  • UI/UX insights
  • And yes, more dark mode appreciation posts ?

Drop by and say hi! Always excited to connect with fellow developers who care about building better web experiences.


The above is the detailed content of Build a Dark Mode Toggle in inutes (That Actually Works). For more information, please follow other related articles on the PHP Chinese website!

Statement
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn
@keyframes vs CSS Transitions: What is the difference?@keyframes vs CSS Transitions: What is the difference?May 14, 2025 am 12:01 AM

@keyframesandCSSTransitionsdifferincomplexity:@keyframesallowsfordetailedanimationsequences,whileCSSTransitionshandlesimplestatechanges.UseCSSTransitionsforhovereffectslikebuttoncolorchanges,and@keyframesforintricateanimationslikerotatingspinners.

Using Pages CMS for Static Site Content ManagementUsing Pages CMS for Static Site Content ManagementMay 13, 2025 am 09:24 AM

I know, I know: there are a ton of content management system options available, and while I've tested several, none have really been the one, y'know? Weird pricing models, difficult customization, some even end up becoming a whole &

The Ultimate Guide to Linking CSS Files in HTMLThe Ultimate Guide to Linking CSS Files in HTMLMay 13, 2025 am 12:02 AM

Linking CSS files to HTML can be achieved by using elements in part of HTML. 1) Use tags to link local CSS files. 2) Multiple CSS files can be implemented by adding multiple tags. 3) External CSS files use absolute URL links, such as. 4) Ensure the correct use of file paths and CSS file loading order, and optimize performance can use CSS preprocessor to merge files.

CSS Flexbox vs Grid: a comprehensive reviewCSS Flexbox vs Grid: a comprehensive reviewMay 12, 2025 am 12:01 AM

Choosing Flexbox or Grid depends on the layout requirements: 1) Flexbox is suitable for one-dimensional layouts, such as navigation bar; 2) Grid is suitable for two-dimensional layouts, such as magazine layouts. The two can be used in the project to improve the layout effect.

How to Include CSS Files: Methods and Best PracticesHow to Include CSS Files: Methods and Best PracticesMay 11, 2025 am 12:02 AM

The best way to include CSS files is to use tags to introduce external CSS files in the HTML part. 1. Use tags to introduce external CSS files, such as. 2. For small adjustments, inline CSS can be used, but should be used with caution. 3. Large projects can use CSS preprocessors such as Sass or Less to import other CSS files through @import. 4. For performance, CSS files should be merged and CDN should be used, and compressed using tools such as CSSNano.

Flexbox vs Grid: should I learn them both?Flexbox vs Grid: should I learn them both?May 10, 2025 am 12:01 AM

Yes,youshouldlearnbothFlexboxandGrid.1)Flexboxisidealforone-dimensional,flexiblelayoutslikenavigationmenus.2)Gridexcelsintwo-dimensional,complexdesignssuchasmagazinelayouts.3)Combiningbothenhanceslayoutflexibilityandresponsiveness,allowingforstructur

Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)Orbital Mechanics (or How I Optimized a CSS Keyframes Animation)May 09, 2025 am 09:57 AM

What does it look like to refactor your own code? John Rhea picks apart an old CSS animation he wrote and walks through the thought process of optimizing it.

CSS Animations: Is it hard to create them?CSS Animations: Is it hard to create them?May 09, 2025 am 12:03 AM

CSSanimationsarenotinherentlyhardbutrequirepracticeandunderstandingofCSSpropertiesandtimingfunctions.1)Startwithsimpleanimationslikescalingabuttononhoverusingkeyframes.2)Useeasingfunctionslikecubic-bezierfornaturaleffects,suchasabounceanimation.3)For

See all articles

Hot AI Tools

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Undress AI Tool

Undress AI Tool

Undress images for free

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Article

Hot Tools

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

VSCode Windows 64-bit Download

VSCode Windows 64-bit Download

A free and powerful IDE editor launched by Microsoft

SecLists

SecLists

SecLists is the ultimate security tester's companion. It is a collection of various types of lists that are frequently used during security assessments, all in one place. SecLists helps make security testing more efficient and productive by conveniently providing all the lists a security tester might need. List types include usernames, passwords, URLs, fuzzing payloads, sensitive data patterns, web shells, and more. The tester can simply pull this repository onto a new test machine and he will have access to every type of list he needs.

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SAP NetWeaver Server Adapter for Eclipse

SAP NetWeaver Server Adapter for Eclipse

Integrate Eclipse with SAP NetWeaver application server.