Creating a dark theme switch with Tailwind & Framer Motion
Tags: javascript, react, tailwindcss, framermotion
Published: 2022-03-25 12:57:15

Dark themes are all the rage, most of the sites you visit today will have some sort of dark theme switch. Allowing you to switch between a light theme and a dark theme on the site you're visiting.

I will hopefully explain how to create an awesome switch using a little bit of Tailwind and Frame Motion. Framer motion is an animation library for React, it's super cool and I recommend that you check it out.

This is what we will be knocking up today.


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kd1m57abwvjormf2kphu.png)

First let's install framer and then import it into our component

```bash
npm install framer-motion
```

Once installed let's add it to our component.

```js
import { motion } from "framer-motion"
```

We need to then import `useState` from React so we can capture the state of `isOn` our component should look something like this now.

```js
import React, { useState} from 'react'
import {motion} from 'framer-motion'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    return()
}
```

Above we have a state of `false` to `isOn` we're currently returning nothing but let's change that now. 

If you take a look at the Framer [example](https://codesandbox.io/s/framer-motion-2-layout-animations-kij8p?from-embed=&file=/src/App.js) it looks very straightforward. With the example, they're using vanilla CSS. Let's use Tailwind CSS with ours.

First, we need to create a container `div` for our switch.

```js
<div className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}></div>
```

I have included a ternary operator in my `className` string this is because we need to conditional move the switch when `isOn` is true or false. 

```js
${isOn && 'place-content-end'}`}
```

We're using **place-content-end** here which allows us to place the element at the end of its container. This is similar to using `justify-end` in Tailwind. The other styles in `className` are just for my preference you can change these to what you like. 

Now we have our container div, let's give it some magic. We need to give it an `onClick` attribute. So let's do that now.

```js
<div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}></div>
```

As you can see we have given the `onClick` a function to execute so let's add that and the div container into our component.

```js
import React, { useState} from 'react'
import {motion} from 'framer-motion'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    const toggleSwitch = () => setIsOn(!isOn)

    return(
        <div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}></div>
    )
}
```

What are we doing with then `toggleSwitch` why aren't we setting it true? I will explain that later but for now let's leave it the way it is. Now time to add the switch. With the container div we should just have a rectangle with rounded edges, let's change that now.

This is where motion comes in, we need to create another `div` but this time it will be a `motion.div` this allows us to give it some frame magic. Let's add that below with some classes from Tailwind.

```js
import React, { useState} from 'react'
import {motion} from 'framer-motion'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    const toggleSwitch = () => setIsOn(!isOn)

    return(
        <div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}>

            <motion.div
                className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/90"
                layout
                transition={spring}
            >

            </motion.div>      

        </div>
    )
}
```

We now have out `motion.div` with the additional attributes of `layout` and `transition` let's go through those now.

**layout**: `boolean` | `"position"` | `"size"`

If `true`, this component will automatically animate to its new position when its layout changes. More info [here](https://www.framer.com/docs/component/###layout)

**transition**: Transition

Defines a new default transition for the entire tree. More info [here](https://www.framer.com/docs/motion-config/###transition)

Let's add our `transition` animations, this is going to be an object like so.

```js
const spring = {
  type: 'spring',
  stiffness: 700,
  damping: 30,
}
```

- [spring](https://www.framer.com/docs/transition/#spring): An animation that simulates spring physics for realistic motion.
- [stiffness](https://www.framer.com/docs/transition/###stiffness): Stiffness of the spring. Higher values will create more sudden movement. Set to 100 by default.
- [damping](https://www.framer.com/docs/transition/###damping): Strength of opposing force. If set to 0, spring will oscillate indefinitely. Set to 10 by default.

After adding our `motion.div` and `spring` object we should have something like this:

```js
import React, { useState} from 'react'
import {motion} from 'framer-motion'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    const toggleSwitch = () => setIsOn(!isOn)

    const spring = {
        type: 'spring',
        stiffness: 700,
        damping: 30,
    }

    return(
        <div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}>

            <motion.div
                className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/90"
                layout
                transition={spring}
            >

            </motion.div>      

        </div>
    )
}
```

This would be our finished switch, but wait there is more..what about the icons and the cool click animation??? Ok, so let's install [React Icons](https://react-icons.github.io/react-icons/) and grab those icons.

Install React Icons via npm.

```bash
npm install react-icons --save
```

I have chosen the following icons, they're from the Remix library. Let's add those now.

```js
import React, { useState} from 'react'
import {motion} from 'framer-motion'
import {RiMoonClearFill, RiSunFill} from 'react-icons/ri'
...
```

Now we need to place our icons, inside of our toggle switch. Our toggle switch is the `motion.div` we made earlier. This stage is pretty simple, we just need to create another `motion.div` inside of the parent `motion.div` and give it some ternary operators and a `whileTape` attribute like so:

```js
<motion.div whileTap={{rotate: 360}}>
    {isOn ? (<RiSunFill className="h-6 w-6 text-yellow-300" />) : (<RiMoonClearFill className="h-6 w-6 text-slate-200" />)}
</motion.div>
```

You can give your icons your own styling but this is how I have set mine up. Using the ternary operator allows us to switch the icon on the status of `isOn` we should now have the following:

```js
import {motion} from 'framer-motion'
import React, {useState} from 'react'
import {RiMoonClearFill, RiSunFill} from 'react-icons/ri'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    const toggleSwitch = () => setIsOn(!isOn)

    const spring = {
        type: 'spring',
        stiffness: 700,
        damping: 30,
    }

    return(
        <div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}>

            <motion.div
                className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/90"
                layout
                transition={spring}
            >
                <motion.div whileTap={{rotate: 360}}>
                    {isOn ? (<RiSunFill className="h-6 w-6 text-yellow-300" />) : (<RiMoonClearFill className="h-6 w-6 text-slate-200" />)}
                </motion.div>

            </motion.div>      

        </div>
    )
}
```

### Adding into Local Storage

Now we have a working component, but it's not completely done we need to handle our dark mode with `localStrogae` so the user can keep their preference for next time. Reading over the [Tailwind Docs](https://tailwindcss.com/docs/dark-mode) on dark mode, we need to be able to toggle dark mode manually. To do this we need to add ` darkMode: 'class',` into our `tailwind.config.js` file. Something like this.

```js
module.exports = {
  darkMode: 'class',
  ...
```

Now we can toggle dark mode manually via the switch. I have used the example on the Tailwind website for supporting light mode, dark mode, as well as respecting the operating system preference. However I have tweaked it a little bit, remember the state `const [isOn, setIsOn] = useState(false)` lets change that to read `localStorage` and check if the `theme` is set to `light`

```js
// before
const [isOn, setIsOn] = useState(false)

// after
const [isOn, setIsOn] = useState(() => {
    if (localStorage.getItem('theme') === 'light') {
      return true
    } else {
      return false
    }
  })
```

Instead of the state returning `false` it fires off a function and checks if the theme within local storage is `light` if it is, `isOn` is true if not it's false. Now let's use the state of `isOn` to manage the theme within local storage.

```js
if (isOn) {
    document.documentElement.classList.remove('dark')
    localStorage.setItem('theme', 'light')
  } else {
    document.documentElement.classList.add('dark')
    localStorage.setItem('theme', 'dark')
  }
```

The above will do the following:

```html
<!-- Dark mode not enabled -->
<html>
<body>
  <!-- Will be white -->
  <div class="bg-white dark:bg-black">
    <!-- ... -->
  </div>
</body>
</html>

<!-- Dark mode enabled -->
<html class="dark">
<body>
  <!-- Will be black -->
  <div class="bg-white dark:bg-black">
    <!-- ... -->
  </div>
</body>
</html>
```

Lastly, we add the following which allows us to avoid [FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content#:~:text=A%20flash%20of%20unstyled%20content,before%20all%20information%20is%20retrieved.) when changing themes of page loads

```js
 if (
    localStorage.theme === 'light' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
  ) { document.documentElement.classList.add('dark') } 
  else {
    document.documentElement.classList.remove('dark')
}
```

So that's it...our final component should look like this...

```js
import {motion} from 'framer-motion'
import React, {useState} from 'react'
import {RiMoonClearFill, RiSunFill} from 'react-icons/ri'

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(() => {
      if (localStorage.getItem('theme') === 'light') {
        return true
      } else {
        return false
      }
    })

    const toggleSwitch = () => setIsOn(!isOn)

    const spring = {
        type: 'spring',
        stiffness: 700,
        damping: 30,
    }

    if (isOn) {
      document.documentElement.classList.remove('dark')
      localStorage.setItem('theme', 'light')
    } else {
      document.documentElement.classList.add('dark')
      localStorage.setItem('theme', 'dark')
    }

    if (
        localStorage.theme === 'light' || (!('theme' in localStorage) && window.matchMedia('(prefers-color-scheme: light)').matches)
      ) { document.documentElement.classList.add('dark') } 
      else {
        document.documentElement.classList.remove('dark')
    }

    return(
        <div onClick={toggleSwitch} className={`flex-start flex h-[50px] w-[100px] rounded-[50px] bg-zinc-100 p-[5px] shadow-inner hover:cursor-pointer dark:bg-zinc-700 ${ isOn && 'place-content-end'}`}>

            <motion.div
                className="flex h-[40px] w-[40px] items-center justify-center rounded-full bg-black/90"
                layout
                transition={spring}
            >
                <motion.div whileTap={{rotate: 360}}>
                    {isOn ? (<RiSunFill className="h-6 w-6 text-yellow-300" />) : (<RiMoonClearFill className="h-6 w-6 text-slate-200" />)}
                </motion.div>

            </motion.div>      

        </div>
    )
}
```