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:

Toggle Preview

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