Home > Article > Web Front-end > 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:
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.
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:
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:
Ready to get your hands dirty? Let's start with the foundation!
Alright, let's get our hands dirty! First, we'll set up the basic structure.
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:
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:
Want to make it even better? Let's move on to some helpful tips that'll take your toggle from good to great!
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.
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(); } });
Disable transitions when user prefers reduced motion:
@media (prefers-reduced-motion: reduce) { body { transition: none; } }
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 data-theme-image data-light-src="/path/to/light-logo.png" data-dark-src="/path/to/dark-logo.png" alt="Build a Dark Mode Toggle in inutes (That Actually Works)">
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; }
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' ); } });
Before you ship, make sure to test these scenarios:
<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>
// 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.
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:
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.
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!
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:
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!