Build a JavaScript and Tailwind CSS Theme Switcher 
Tags: javascript, tailwindcss, css, webdev
Published: 2021-02-04 10:24:36

**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](https://dev-to-uploads.s3.amazonaws.com/i/zkb6f5mtu2hydozqvwj3.png)

>Check out the [DEMO](https://new.brandymedia.co.uk/)
Download the [SOURCE CODE](https://github.com/brandymedia/tailwind-theme-switcher)

{% github brandymedia/tailwind-theme-switcher no-readme %}

##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](https://twitter.com/thirusofficial)**.

You can clone or download this directly from GitHub - [https://github.com/ThirusOfficial/tailwind-css-starter-postcss](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.

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

```html
<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*.

```javascript
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:

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

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

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

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

```javascript
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:

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.

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

```javascript
document.documentElement.classList.add('dark');
```

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

```javascript
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:

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

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

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

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

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

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

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

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

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

```css
.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](https://twitter.com/brandymedia) 👍🏻