DEV Community

loading...
Cover image for Build a JavaScript and Tailwind CSS Theme Switcher

Build a JavaScript and Tailwind CSS Theme Switcher

brandymedia profile image Andy Griffiths ・10 min read

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.

Alt Text

Check out the DEMO
Download the SOURCE CODE

GitHub logo 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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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',
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

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
});
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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:

  1. Dark mode has been selected in the theme switcher, so localTheme will equal dark
  2. Light mode has been selected in the theme switcher, so localTheme will equal light
  3. 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
}
Enter fullscreen mode Exit fullscreen mode

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');
Enter fullscreen mode Exit fullscreen mode

If the localTheme is set to Light then we need to remove the dark class from the root element.

document.documentElement.classList.remove('dark');
Enter fullscreen mode Exit fullscreen mode

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');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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');
    }
}
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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();
})
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

Add the necessary CSS styles to the src/styles.css file.

.is-active {
    border: 2px solid rgb(107, 114, 128)!important;
}
Enter fullscreen mode Exit fullscreen mode

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 👍🏻

Discussion (5)

pic
Editor guide
Collapse
michaelandreuzza profile image
michael-andreuzza

Hey Andy!

This great and I thank you for it.

I am reworking Wicked Blocks

And I am implementing this, bit then I am implementing it as a select component,like on Vercel.

It didn't work I think I am missing something,but not sure what.

Any suggestion?

Thank you so much

Collapse
brandymedia profile image
Andy Griffiths Author

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.

Collapse
michaelandreuzza profile image
michael-andreuzza

every is working as you have implemented, but what I was wondering is that when using
´´´select
option
option
option´´´

is not working. I might be missing something

Thread Thread
brandymedia profile image
Andy Griffiths Author

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...

Thread Thread
michaelandreuzza profile image
michael-andreuzza

Oh, true. I could try that
Thank you Andy.