DEV Community

Cover image for Toggle Dark Mode in React
Abbey Perini
Abbey Perini

Posted on Edited on

Toggle Dark Mode in React

When I rebuilt my portfolio site, I knew I wanted to have some fun with the design, and a dark and light mode fit the bill. I enjoyed a lot of the discoveries I made during this project, but my favorite has to be the color changing SVGs. This tutorial assumes familiarity with React, and I am using v17.0.1 and functional components.

First, I created a base layout. Next, I put together my dark and light color schemes. It took a little trial and error, but after testing all my combinations for sufficient contrast and experimenting with placement, I found I needed 6 CSS variables. I guess you could say I used โ€œdark firstโ€ development, because the variable names make sense in the context of the dark theme. The light theme has less variation, but needed --button-border where --accent would be the same color as the background.

.theme-dark {
  --dark-text: #292929;
  --light-text: #F9F8F8;  
  --dark-background: #2F4550;
  --light-background: #586F7C;
  --accent: #B8DBD9;
  --button-border: #B8DBD9;
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
.theme-light {
  --dark-text: #5E4B56;
  --light-text: #5E4B56;
  --dark-background: #DBE7E4;
  --light-background: #EDDCD2;
  --accent: #DBE7E4;
  --button-border: #5E4B56;
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

Then, I set about applying colors to my base layout:

html, #root {
  background-color: var(--dark-background);
  color: var(--dark-text);
}

nav {
  background-color: var(--dark-background);
  color: var(--light-text);
}

.main-container {
  background-color: var(--light-background);
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

I also set the backgrounds of the sections of content that I wanted to pop to --accent. --dark-text would have worked on all backgrounds in the dark theme, but I set the section titles to --light-text to make them stand out more.

I found Musthaq Ahamadโ€˜s basic theme switcher tutorial, and set about applying it to functional React components.
I put functions for changing the theme and checking localStorage for theme preferences into a file called themes.js.

function setTheme(themeName) {
    localStorage.setItem('theme', themeName);
    document.documentElement.className = themeName;
}

function keepTheme() {
  if (localStorage.getItem('theme')) {
    if (localStorage.getItem('theme') === 'theme-dark') {
      setTheme('theme-dark');
    } else if (localStorage.getItem('theme') === 'theme-light') {
      setTheme('theme-light')
    }
  } else {
    setTheme('theme-dark')
  }
}

module.exports = {
  setTheme,
  keepTheme
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

In my App.js file, I added keepTheme() to my useEffect().

import { keepTheme } from './utils/themes';

function App() {
  useEffect(() => {
      keepTheme();
  })
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

Next, I added the toggle component to my navigation bar component. I styled the toggle following Chris Bongersโ€™ Tutorial based on Katia De Juanโ€™s Dribbble. Then I adjusted the size and flipped it to default to dark mode. While this toggle is so cute that you could die, this tutorial will work with any <button> or clickable <input>. First, I set up the basic JSX, the local state, and a variable to hold the theme we get from localStorage:

import React, { useEffect, useState } from 'react';
import '../styles/toggle.css';
import { setTheme } from '../utils/themes';

function Toggle() {
  const [togClass, setTogClass] = useState('dark');
  let theme = localStorage.getItem('theme');
  return (
        <div className="container--toggle">
           <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
            <label htmlFor="toggle" className="toggle--label">
                <span className="toggle--label-background"></span>
            </label>
        </div>
    )
}

export default Toggle;
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

When a user clicks the toggle, I want the theme on the page to change and the toggle to change with it. I added the imported setTheme() function and setTogClass() from the local state to a handleOnClick function. You can see where it is passed to the clickable part of the toggle in the JSX above.

const handleOnClick = () => {
  if (localStorage.getItem('theme') === 'theme-dark') {
      setTheme('theme-light');
      setTogClass('light')
  } else {
      setTheme('theme-dark');
      setTogClass('dark')
  }
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

I used this componentโ€™s useEffect() to make sure the local state togClass always loads with the correct theme.

useEffect(() => {
    if (localStorage.getItem('theme') === 'theme-dark') {
        setTogClass('dark')
    } else if (localStorage.getItem('theme') === 'theme-light') {
        setTogClass('light')
    }
}, [theme])
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

Because my toggle is a checkbox, the dark theme should show the unchecked (moon) state and the light theme should show the checked (sun) state. I couldnโ€™t get defaultChecked to work how I wanted, so I replaced the unchecked <input> with this conditional rendering ternary operator (conditional operator):

{
    togClass === "light" ?
    <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
    :
    <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

If you used a <button>, you could easily use conditional rendering like this to change the className attribute within the <button> tag and get the same effect.

Put all together, the code for the toggle component looks like this:

import React, { useEffect, useState } from 'react';
import '../styles/toggle.css';
import { setTheme } from '../utils/themes';

function Toggle() {
    const [togClass, setTogClass] = useState('dark');
    let theme = localStorage.getItem('theme');

    const handleOnClick = () => {
        if (localStorage.getItem('theme') === 'theme-dark') {
            setTheme('theme-light');
            setTogClass('light')
        } else {
            setTheme('theme-dark');
            setTogClass('dark')
        }
    }

    useEffect(() => {
        if (localStorage.getItem('theme') === 'theme-dark') {
            setTogClass('dark')
        } else if (localStorage.getItem('theme') === 'theme-light') {
            setTogClass('light')
        }
    }, [theme])

    return (
        <div className="container--toggle">
            {
                togClass === "light" ?
                <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked />
                :
                <input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} />
            }
            <label htmlFor="toggle" className="toggle--label">
                <span className="toggle--label-background"></span>
            </label>
        </div>
    )
}
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

Update

To see how I have refactored the logic of this component and made it accessible, read An Accessible Dark Mode Toggle in React.

Finally, my favorite part: the color switching SVGs! CSS variables work in SVG code too!

I got my SVG code for the Github and Chrome icons from DEVICON. For the Github icon all I had to change was one fill attribute in a <g>:

<g fill="var(--dark-text)">
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

The Chrome icon had a fill attribute in a <circle> and a <path>:

<circle fill="var(--dark-text)" cx="63.624" cy="64.474" r="22.634"></circle><path fill="var(--dark-text)" ...>
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026

The result looks like this:
desktop - toggle looks like a moon with stars click changes the site from dark mode to light mode and the toggle now looks like a sun with clouds
mobile - toggle looks like a moon with stars click changes the site from dark mode to light mode and the toggle now looks like a sun with clouds

Conclusion

I tried to include all of the relevant code, but you can also see the full code for my site in its Github repository. If you enjoyed this article or are left with questions, please leave a comment! I would also love to see anything built following this tutorial.

Top comments (19)

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
kirkcodes profile image
Kirk Shillingford •

I will 100% be referencing this article multiple times. Thank you for putting this together.

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
overtureweb profile image
Overture Web •

This was super helpful, I love it! Thanks for taking the time to go through it. I found a place to simplify, rather than use a ternary operator that renders one of two input elements you could use an equality check expression as the checkbox inputโ€™s โ€œcheckedโ€ property.

<input type="checkbox" id="toggle" className="toggle--checkbox" onClick={handleOnClick} checked={togClass === โ€œlightโ€} />
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026 ๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini • • Edited

Ah, but the reason "I couldnโ€™t get defaultChecked to work how I wanted, so I replaced the unchecked with this conditional rendering ternary operator (conditional operator):" is the togClass doesn't just depend on whether the checkbox is checked. It also depends on the theme set in localStorage. Otherwise, the user could have light mode enabled by default or have set the theme to light mode and reloaded. Then the "checked=false" state will not match the current set theme and you'll see a moon on a light background. I was trying to tell the element to be checked or unchecked using a conditional in the defaultChecked property in the element, which made React yell about an uncontrolled element. The ternary operator renders 1 element either in the checked or unchecked state, which will not cause React to yell about it being uncontrolled.

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

You were totally right! I just didn't understand what you were saying. You can see the updated logic here: dev.to/abbeyperini/an-accessible-d...

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
urielbitton profile image
Uriel Bitton •

nice, comprehensive, and well written article!

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

Thank you!

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

Hmm. I chose not to use Redux for this project.

The only time I used local state was to get the toggle to load with the correct side. You could put that in global state, but I don't see an advantage to that - especially since you could probably get that to work with just JS. ๐Ÿค”

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
passenger89 profile image
William Nicholson •

Nicely done. I couldn't help but notice you are using hooks inside conditional statements. Isn't that not recommended in rules of hooks?

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

According to the link you provided and my experience working with the ESLint rule, this code meets those recommendations. The order in which the hooks are called will not change from one render to another in these components.

The ones in useEffect() match an example in the link you provided.

The click handler will not be called on render. Plus, I even have conditions for all the possible values of the variable I'm checking against. This is a common design.

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
viragdesai profile image
Virag Dilip Desai •

Very well written and explained. Thank you!

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

Thanks for reading!

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
mnunezdm profile image
Miguel Nรบรฑez Dรญaz-Montes •

Why are you doing this?

localStorage.getItem('theme') && localStorage.getItem('theme') === 'theme-dark'

And not directly this?

localStorage.getItem('theme') === 'theme-dark'

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

"if (localStorage.getItem('theme') && localStorage.getItem('theme') === 'theme-dark')" in keepTheme() is checking if a theme is stored in localStorage before comparing it to a string. It's in the main container's useEffect() and was throwing an error if it didn't exist when written like you suggest. The handleOnClick() in the Toggle component is written the way you suggest because there's always something in localStorage by the time it gets used.

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini • • Edited

Now it occurs to me I could totally refactor it to

if (localStorage.getItem('theme')) {if (localStorage.getItem('theme') === 'theme-dark') {setTheme('theme-dark'); } else if (localStorage.getItem('theme') === 'theme-light') {setTheme('theme-light')

} else {
setTheme('theme-dark')
}
}

ETA: the site and blog have been updated with this refactor

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
ruppysuppy profile image
Tapajyoti Bose •

Great job! Really liked the animation & stars and the cloud

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
lindiwe09 profile image
Lindiwe Dokotala •

Nice and well-written article but any idea on how l can implement that using Tailwind CSS

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
abbeyperini profile image
Abbey Perini •

No, but if I figure it out on the upcoming project I'm going to learn Tailwind for, I'll let you know.

๐Ÿ”ด Live TV: abbeyperini/toggle dark mode in react c - Info Live Streaming 2026
 
lindiwe09 profile image
Lindiwe Dokotala •

Alrighty

Some comments may only be visible to logged-in visitors. Sign in to view all comments. Some comments have been hidden by the post's author - find out more