Build an React toggle switch for Dark Mode
Last night while working on this site in my home office, with the lights dimmed, I've realized how bright it looked. I have my MacBook set to dark mode, and my page was standing out too much for my taste, so I decided to implement Dark Mode.
Tools Used
My site is built using Gatsby and TailwindCSS:
Gatsby
If you haven’t used Gatsby before, then I highly recommend it! It’s React-based framework for creating websites and apps. It's great whether you're building a portfolio site or blog, or a high-traffic e-commerce store or company homepage.
Although this tutorial uses Gatsby/React, it should be very simple to convert this design to any framework as it just uses a single variable.
TailwindCSS
Also if you’ve not heard of tailwindcss it’s a utility-first CSS framework and it’s incredibly powerful! In simple terms you use lot of very specific class names to shape the design of your site, for example ml-0
is 0px left margin
and pt-2
is 0.5rem padding top
.
One of the more powerful features is how it allows me to make my layouts responsive without the need to write a single media query! For example I can do the following:
<div class="w-full md:w-1/2"> ... </div>
Which gives a width 100% as a base and width 50% after 768px browser width. How cool!
Gatsby Dark Mode Plugin (gatsby-plugin-dark-mode)
A Gatsby plugin which handles some of the details of implementing a dark mode theme. It provides:
- Browser code for toggling and persisting the theme (from Dan Abramov’s overreacted.io implementation)
- Automatic use of a dark mode theme (via the prefers-color-scheme CSS media query) if you’ve configured your system to use dark colour themes when available.
- A React component for implementing theme toggling UI in your site.
Let’s code!
Ok, enough prelude, lets dig into some code! 💻
Step 1 - The Toggle Outline
Our first step is to build the underlying HTML structure of the toggle switch. We will start with the basic gatsby-plugin-dark-mode outline, slightly modified to use functional components.
import React from 'react'
import { ThemeToggler } from "gatsby-plugin-dark-mode"
function ThemeToggle() {
return (
<ThemeToggler>
{({ theme, toggleTheme }) => (
<label>
<input
type="checkbox"
onChange={e => toggleTheme(e.target.checked ? 'dark' : 'light')}
checked={theme === 'dark'}
/>{' '}
Dark mode
</label>
)}
</ThemeToggler>
)
}
export default ThemeToggle
Here it is important to remember that we should always try to make any interfaces we build as easy to use as possible to use.
We are shooting for this:
To get there we will need a Container, a Lane for the switch, and the Switch itself. Let's leave the input[type=checkbox]
there for now:
<div> {/* Container */}
<label> {/* Lane */}
<input
type="checkbox"
onChange={e => toggleTheme(e.target.checked ? 'dark' : 'light')}
checked={theme === 'dark'}
/>
<div /> {/* Switch */}
</label>
</div>
Ok, so as you can see we now have a div
that contains our toggle switch and then a switch (inside the lane). We can now start building on this template using tailwindcss.
Container
Our first task is to ensure that our title and switch sit in a line, currently they are one above and the other below. To do this we give the container the following classes:
flex justify-between items-center
flex
gives us block content on the same line.
justify-between
places all the extra space inside a div in-between the elements in a container. By doing this it means the title should always be right on the left edge of the container and the toggle switch should be on the right.
items-center
means our content is aligned vertically.
Lane
Our next task is to style the Lane label. Let’s add the following classe:
w-14 h-8 flex items-center rounded-full p-1
w-14 h-8
gives use the width and height dimensions for the switch container.
bg-gray-300
gives us a nice grey background.
rounded-full
gives use a nice capsule shaped container with rounded edges.
flex-shrink-0
prevents the container width from shrinking in size, this sometimes occurs if the browser width is very small.
p-1
gives us a small amount of padding around our ‘switch container’ so it doesn’t touch the edge.
Switch
Next, we need to add the actual switch inside the switch container. Ok to do this let’s add the following classes to the Switch div:
bg-white w-6 h-6 rounded-full shadow-md
bg-white w-8 h-8 rounded-full
will give us a white div thats rounded.
shadow-md
is an added extra that will give us a small drop shadow around the bottom of the switch. This is an optional class.
Awesome! The base work is done, so let’s recap what we have done so far.
Round Up
Lastly, we need to hide the input[type=checkbox]
by adding the hidden
class to it.
Here is a round up of how our code should be looking so far:
<div className="flex justify-between items-center"> {/* Container */}
<label className="w-14 h-8 flex items-center rounded-full p-1"> {/* Lane */}
<input
type="checkbox"
className="hidden"
onChange={e => toggleTheme(e.target.checked ? 'dark' : 'light')}
checked={theme === 'dark'}
/>
<div className="bg-white w-6 h-6 rounded-full shadow-md" /> {/* Switch */}
</label>
</div>
Ok, nice work if we look at the what we have so far we should see something resembling a toggle switch.
Step 2 - Functionality
Our base component is in, now time to add some functionality to the toggle switch.
This part is relatively simple, since we’re alredy listening for changes to the input[type=checkbox]
. When we click the label
we pass either dark
or light
to the toggleTheme
function, that in turn returns the theme
back to us.
We can check theme
and add some extra classes to our elements:
{ theme === 'dark' ? "..." : "..." }
With that we can add the toggle states to our component. Let’s change the background colour of the label when the switch is either in light or dark mode. This will be better for user experience as we will be able to see which toggles are activated at a glance.
<label className={`w-14 h-8 flex items-center rounded-full p-1 ${ theme === 'dark' ? "bg-indigo-500" : "bg-yellow-200"}`}>
Now we need to add a transform translate-x-…
classes to the switch to move the switch horizontally to the right. Let’s add the transform translate-x-6
class to the switch, when in dark mode, and see how it looks.
<div className={`bg-white w-6 h-6 rounded-full shadow-md ${theme === 'dark' ? "transform translate-x-6" : ""}`} />
Missing transform
from my animations was a common gotcha’ I stumbled on when I first started using tailwind.
Congratulations 🎉 you have a functioning toggle switch!
Animation
Everything works, but it’s not very elegant…
Ok, so the toggle switch is now working but the toggle currently jumps from left to right instantly. Let’s give it some finesse. First let’s add a transition-duration and a transition-timing-function class.
To the class of the switch add the following classes:
duration-300 ease-in-out
duration-300
sets an animation duration time of 300ms, so it will take 300ms for the switch to move from left to right.
ease-in-out
changes the animation so that the start and the end movements are much slower than the movement during the middle of the animation, making it feel more natural. There is a great explanation of how this alters the animation here.
Our switch code now looks like so:
<div className={`bg-white w-6 h-6 rounded-full shadow-md duration-300 ease-in-out ${theme === 'dark' ? "transform translate-x-6" : ""}`} />
TLDR;
Below is the code for a nice little Dark Mode toggle switch built with react and tailwindcss.
Key points:
- Use translate for movement over position / margin
- Don’t forget to add the
transform
class when using transforms for animations
Complete React Dark Mode Component
import React from 'react'
import { ThemeToggler } from "gatsby-plugin-dark-mode"
function ThemeToggle() {
return (
<ThemeToggler>
{({ theme, toggleTheme }) => (
<div className="flex justify-between items-center">
<label className={`w-14 h-8 flex items-center rounded-full p-1 ${ theme === 'dark' ? "bg-indigo-500" : "bg-yellow-200"}`}>
<input type="checkbox" className="hidden"
onChange={e => toggleTheme(e.target.checked ? 'dark' : 'light')}
checked={theme === 'dark'}
/>
<div className={`bg-white w-6 h-6 rounded-full shadow-md duration-300 ease-in-out ${theme === 'dark' ? "transform translate-x-6" : ""}`} />
</label>
</div>
)}
</ThemeToggler>
)
}
export default ThemeToggle