DEV Community

Paul Bennett
Paul Bennett

Posted on

Creating a dark theme switch with Tailwind & Framer Motion

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

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



npm install framer-motion


Enter fullscreen mode Exit fullscreen mode

Once installed let's add it to our component.



import { motion } from "framer-motion"


Enter fullscreen mode Exit fullscreen mode

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



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

export default function DarkModeSwitch(){

    const [isOn, setIsOn] = useState(false)

    return()
}


Enter fullscreen mode Exit fullscreen mode

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



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


Enter fullscreen mode Exit fullscreen mode

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.



${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>
    )
}
```
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
seoakbarali profile image
akbarali seo

Alight Motion is a powerful motion design app that lets users create professional-quality animations and visual effects right on their mobile devices. It offers a wide range of features, including keyframe animation, blending modes, vector graphics, and more. Click now to explore its capabilities and take your video editing to the next level!