Working on a screen all day (and often night), your eyes can take a real battering. In September 2019, Apple released Dark Mode on iOS 13, I've not looked back since.
At first, not all apps supported this but slowly over the subsequent months to follow, a lot more have seen the light; or in this case, turned it out.
Check out the DEMO
Download the SOURCE CODE
brandymedia / tailwind-theme-switcher
A JavaScript & Tailwind CSS theme switcher.
Fad or Fab
Following in the steps of native mobile apps, websites have also seen a surge in theme switchers allowing their users to switch between light and dark modes.
On the surface, this may seem a little novel and gimmicky. In reality, I actually think there's some real utility in offering protection for your users' eyes.
Personally, I’ve suffered with migraines and headaches over the years and even the slightest respite from unnecessary screen brightness is always welcomed.
What We’re Going to Build
With rapid advancements in modern JavaScript and the popularity of the Utility First CSS framework Tailwind CSS, I thought it would be fun and also useful to combine the 2 to build a theme switcher.
The theme switcher will have 3 modes - dark, light and auto. The first 2 are pretty self-explanatory. The third auto option is going to utilise JavaScript's window.matchMedia
method. This will detect the display preferences of the user's device to automagically select either dark or light accordingly.
Luckily Tailwind CSS already supports dark mode out-of-the-box, so most of the heavy lifting will be done in JavaScript, albeit in under 60 lines of code so don’t worry.
No need to Reinvent the Wheel
To boost our productivity straight out of the gate, we’re going to be using the excellent Tailwind CSS and PostCSS starter template from Shruti Balasa @thirusofficial.
You can clone or download this directly from GitHub - https://github.com/ThirusOfficial/tailwind-css-starter-postcss then follow the set up instructions in the README.md
file.
This will give us an environment set up ready to use where we can compile Tailwind CSS with ease.
Getting Down to Business
Once you’ve got your copy of the starter template set up, it’s time to get stuck in and write the markup and JavaScript we’ll need to get this working.
First step, create our index.html
and app.js
files:
touch public/index.html
touch public/app.js
I’m using Visual Studio Code for my code editor which has built in support for Emmet which speeds up your workflow when writing your HTML.
In our index.html
file, type ! tab
. This will give us our HTML boilerplate code.
Next, we’ll update our title tag to Theme Switcher
and then call our javascript & css files and add Font Awesome for some icons.
<link rel="stylesheet" href="https://pro.fontawesome.com/releases/v5.10.0/css/all.css" integrity="sha384-AYmEC3Yw5cVb3ZcuHtOA93w35dYTsvhLPVnYs9eStHfGJvOvKxVfELGroGkvsg+p" crossorigin="anonymous"/>
<link rel="stylesheet" href="dist/styles.css">
<script defer src="app.js"></script>
Notice that the link to our CSS includes dist
as this is where PostCSS outputs our compiled CSS.
Before writing the JavaScript which will give us our interactivity, we’ll need to first write our HTML within our index.html
file.
Nothing too scary here, just basic HTML tags styled with Tailwinds CSS utility classes.
<div class="flex w-full justify-around items-center fixed bg-gray-100 dark:bg-gray-900 text-gray-900 dark:text-white py-5">
<div class="theme-switcher">
<button class="theme-switcher-button theme-switcher-light bg-gray-200 border-gray-200 border-2 dark:bg-black dark:border-black p-2 focus:outline-none" title="Light"><i class="fas fa-sun pointer-events-none"></i> Light</button><button class="theme-switcher-button theme-switcher-dark bg-gray-200 border-gray-200 border-2 dark:bg-black dark:border-black p-2 focus:outline-none" title="Dark"><i class="fas fa-moon pointer-events-none"></i> Dark</button><button class="theme-switcher-button theme-switcher-auto bg-gray-200 border-gray-200 dark:bg-black border-2 dark:border-black p-2 focus:outline-none" title="Auto"><i class="fas fa-adjust pointer-events-none"></i> Auto</button>
</div>
</div>
<div class="flex w-full h-screen justify-center items-center bg-white dark:bg-gray-800">
<h1 class="text-5xl text-gray-900 dark:text-white">Hello World!</h1>
</div>
There may look like there’s a ton of code here. The HTML is actually quite small but the way Tailwind works, it uses lots of CSS classes to style the elements, so it can look quite verbose.
Don’t worry too much about this for now. In the main it should be quite self explanatory what each class does, but if you want to learn more then check out the Tailwind CSS docs https://tailwindcss.com/docs.
One class to draw your attention to is the dark: variant class. When the dark class is set on the html or body elements, these utility classes allow us to control the styles for when the user has the Dark mode enabled.
If you manually add the class dark
to the html tag, you will notice this is not quite working yet. We'll need to configure the tailwind.config.js
file first.
Open up tailwind.config.js
which should be in the root of your project directory. Then update darkMode to class.
darkMode: 'class',
Still no luck? That's because we need to recompile Tailwind to make sure the dark variants are added to our styles.css
. So run npm run build
again.
If you check your web page again, you should now see it's switched to dark mode, cool.
However, we can’t expect our website users to manually add the dark class to the markup to change themes, so we need to write the JavaScript to do this automatically when the user toggles the theme.
Remove the dark
class from the html tag, as we don’t need this anymore.
Let’s open up our app.js
file and get cracking.
First thing I like to do to avoid any embarrassing issues later is to make sure the app.js
file is linked up correctly.
In our app.js
file write:
console.log(‘Yep’);
Then in our browser, open up our developer tools and open the console tab.
We should see it outputs Yep - great, this is working, so you can delete the console.log(‘Yep’);
from app.js
now.
The code we’re going to write in our app.js
file is going to consist of 3 main JavaScript concepts; DOM Manipulation, Event Listeners and Functions.
We want to listen for an event when a user clicks the options on our theme switcher and then run the necessary function to update the styles of our page.
To be able to listen for an event and manipulate the DOM, we first need to select the relevant HTML element with our JavaScript and set it inside of a variable so we can access this later on in our code.
We do this by querying the document for a specific element.
const themeSwitcher = document.querySelector('.theme-switcher');
Once we've grabbed our element, we can then add an event lister to detect when the user clicks on our theme switcher.
themeSwitcher.addEventListener('click', (e) => {
// code run when user clicks our element
});
Now we need to write a few functions to hold the code we want to run when the click event is fired.
function getTheme() {
// gets the current theme selected
}
function setTheme() {
// sets the theme
}
function setActive() {
// adds active state to the buttons
}
The default behaviour we want in our code will be to look to see if the user has selected a display preference on their device (light or dark) and then whether they have implicitly set an option using our theme switcher.
If they have selected an option on the theme switcher, then this will take precedence over the device preference.
We’re going to keep track of the users preference using JavaScripts localStorage
property as this allows us to store data across browser sessions, so we can still access this even if the user closes their tab.
So let's work on the getTheme
function first, checking if the user has manually set a preference for their theme.
const localTheme = localStorage.theme;
This code looks in our browsers local storage for the key theme and if it exists, sets our localTheme
variable to the corresponding value.
There are 3 possibilities here:
- Dark mode has been selected in the theme switcher, so
localTheme
will equal dark - Light mode has been selected in the theme switcher, so
localTheme
will equal light - Neither Dark or Light mode have been selected in the theme switcher so we fall back to the device preference if one has been set.
Let's set that conditional code up to catch each case.
if (localTheme === 'dark') {
// user has manually selected dark mode
} else if (localTheme === 'light') {
// user has manually selected light mode
} else {
// user has not manually selected dark or light
}
The logic is now if the localTheme
set in the localStorage
of the browser is set to Dark then we use javascript to set a dark class on the root element of the document, in this case the html element.
document.documentElement.classList.add('dark');
If the localTheme
is set to Light then we need to remove the dark class from the root element.
document.documentElement.classList.remove('dark');
Finally, if there are no themes set locally then we use the auto option, which either adds or removes the class depending on what preference is set on the device.
Our getTheme
function now looks like this:
function getTheme() {
const localTheme = localStorage.theme;
if (localTheme === 'dark') {
document.documentElement.classList.add('dark');
} else if (localTheme === 'light') {
document.documentElement.classList.remove('dark');
} else {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
}
}
If we now call the getTheme
function within the themeSwitcher
event listener, next time we click any of the options, the code will run.
If you give it a try, then you may notice that either nothing changed, or it has changed to dark mode. Currently the way our code is set up, it will look to see if our device has a display preference and then it will set that.
We now need to hook up the buttons, so they can actually set the theme to override our devices default. So let's create our setTheme function.
function setTheme(e) {
// Set our theme choice
}
Notice that we are using a parameter in this function, this is because we need to be able to detect which button we clicked in our theme switcher, so we need to hook in to the event
, or e
for short.
Let’s set the element we’ve clicked in a variable using the events target
property.
let elem = e.target;
Then set up an other conditional block of code to decide what we need to do based off which element was clicked by the user.
function setTheme(e) {
let elem = e.target;
if (elem.classList.contains('theme-switcher-dark')) {
localStorage.theme = 'dark';
} else if (elem.classList.contains('theme-switcher-light')) {
localStorage.theme = 'light';
} else {
localStorage.removeItem('theme');
}
}
To explain the above code in more detail. We’re saying if the user clicks the button with the class theme-switcher-dark
then set the theme locally in localStorage
to dark.
Else if the user clicks the button with the class theme-switcher-light
then set the theme locally in localStorage
to light.
Finally, if the user clicks the auto option, then we remove the theme
key from localStorage
and then we can fall back to the users device default.
To make sure we run the code in this function when a user clicks, we need to call this inside the themeSwitcher
event listener.
themeSwitcher.addEventListener('click', (e) => {
setTheme(e);
getTheme();
});
Notice we pass the event as an argument from the click through the function so we can pick it up in our functions code.
Now we should be able to switch between the light and dark themes with the buttons we created in our HTML. Nearly there.
You’ve probably noticed that if we reload the page when auto is selected, it always defaults to the light theme. We need to make sure we run the getTheme
function when we load the page. We can do this with another event listener.
window.addEventListener('load', () => {
getTheme();
})
The code above listens for the page load event and then runs the function inside, which does the job.
To enable the theme change when the user updates their device settings, without them having to refresh their web page, we can add one last event listener.
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => {
getTheme();
});
At this stage all of our functionality is working as expected but the UI is not great as it's not obvious which option has been selected. Let's fix that.
We will add a setActive
function which will add a is-active
class to the selected button, allowing us to add some CSS styles to identify which option has been selected.
function setActive(selectedButton) {
const themeSwitcherButtons = document.querySelectorAll('.theme-switcher-button');
themeSwitcherButtons.forEach((button) => {
if (button.classList.contains('is-active')) {
button.classList.remove('is-active');
}
})
let activeButton = document.querySelector(`.theme-switcher-${selectedButton}`);
activeButton.classList.add('is-active');
}
In our getTheme
function we will set this up and then call the function.
function getTheme() {
const localTheme = localStorage.theme;
let selectedButton;
if (localTheme === 'dark') {
document.documentElement.classList.add('dark');
logoSvg[0].style.fill = 'rgb(255,255,255)';
selectedButton = 'dark';
} else if (localTheme === 'light') {
document.documentElement.classList.remove('dark');
logoSvg[0].style.fill = 'rgb(0,0,0)';
selectedButton = 'light';
} else {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.classList.add('dark');
logoSvg[0].style.fill = 'rgb(255,255,255)';
selectedButton = 'auto';
} else {
document.documentElement.classList.remove('dark');
logoSvg[0].style.fill = 'rgb(0,0,0)';
selectedButton = 'auto';
}
}
setActive(selectedButton);
}
Add the necessary CSS styles to the src/styles.css
file.
.is-active {
border: 2px solid rgb(107, 114, 128)!important;
}
You'll then need to rebuild your styles with npm run build
.
Once everything has re-compiled, we should be finished with our JavaScript & Tailwind CSS Theme Switcher.
If you enjoyed this article, then please follow me on Twitter for more coding tips and tricks @brandymedia 👍🏻
Top comments (2)
Couple if things that it might be worth checking...
Does you Tailwind compiled CSS contain the dark: variant? This needs to be imported in the tailwind.config.js file by enabling darkMode with 'class'. Your CSS would then need recompiling.
Is your script working, which adds the 'dark' class to the websites html tag. Open up dev tools in your browser and look for the 'dark' class when you've selected dark in the theme switcher.
It may sound obvious but are you adding both a light and dark class to an element, so for example... bg-white dark:bg-gray-800
I don't use components on Vercel so it may be that there is something specific to your set up.
Select boxes are notoriously hard to style. It looks like Tailwind coped out of this, as they use a UL's instead - tailwindui.com/components/applicat...