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.
First let's install framer and then import it into our component
npm install framer-motion
Once installed let's add it to our component.
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.
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 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>
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>
)
}
```
Top comments (1)
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!