Toggle Dark Mode
(updated)
So you are creating your own website and you decide to have a dark and light theme, there might be a few questions that pop on your mind. How do I switch between them? Do I need to store them in a global state manager like Redux or MobX? How to integrate it with my styling solution?
In this blog post I intend on clarifying how I used Context
and useContext
to store the current state of this website’s theme toggle.
To achieve our goal we will use styled-components
and react
. You may switch styled-components to vanilla css
and dynamically assign css-variables in order to effectively change your theme, some adjustments are required, but the main Context
concept will be the same.
Setting Up
We will start by creating two theme objects:
const darkTheme = {
isDark: true,
background: 'black',
primary: 'lightblue',
secondary: 'pink',
}
const lightTheme = {
isDark: false,
background: 'white',
primary: 'royalblue',
secondary: 'palevioletred',
}
Note the isDark
property above, that is there so you can easily check if the theme is dark through the theme
object, which is provided to every styled-component
.
With that done, we will need to check if the user has a saved preference from previous visits and the system’s prefers-color-scheme value. I use localStorage
for storing preference, but you can use any type of storage you want.
// We don't parse it because "unset" is also a valid value
const savedIsDarkMode = localStorage.getItem('dark')
// Here we leverage matchMedia API to check if the system prefers dark mode
const prefersDarkMode = matchMedia('(prefers-color-scheme: dark)').matches
With those values in hand, we can get an usable isDarkMode
value
// Get usable isDarkMode
const getIsDarkMode = () => {
// Checks if user has a preference already set and use it
// If not set, then try to select mode by detecting system preference
switch (savedIsDarkMode) {
case 'true':
return true
case 'false':
return false
default:
return prefersDarkMode
}
}
With these values in hand, we are ready to create the react context necessary to store our dark mode state, this can be achieved with the code bellow
export const ThemeModeContext = React.createContext({
theme: getIsDarkMode() ? darkTheme : lightTheme,
setIsDarkMode: () => {},
})
The ThemeModeContext
can be imported and used with useContext in order to call setIsDarkMode
from within the app. We initialized the values merely as a formality, so we know the object shape.
Who Provides the Providers?
We are ready to create the mighty ThemeModeProvider
, which will encapsulate all of our application’s code.
export const ThemeModeProvider = ({ children }) => {
const [isDarkMode, setIsDarkMode] = useState(getIsDarkMode())
useEffect(() => {
localStorage.setItem('dark', JSON.stringify(isDarkMode))
}, [isDarkMode])
const mode = {
theme: isDarkMode ? darkTheme : lightTheme,
setIsDarkMode,
}
return (
<ThemeModeContext.Provider value={mode}>
<ThemeProvider theme={mode.theme}>{children}</ThemeProvider>
</ThemeModeContext.Provider>
)
}
Take a look at the code above, it looks quite simple if you are comfortable with hooks and context, but we are going to dissect the component anyway for those who missed on that.
First we have useState
which was initialized with the value from getIsDarkMode()
. We do this so the state always has the correct value on initialization. Assuming getIsDarkMode()
returns true
, we then have:
const [isDarkMode, setIsDarkMode] = useState(getIsDarkMode())
// [true, (state) => newState]
This is the basics of useState. It takes in an initial value and returns an array with the signature [currentState, updateStateFunction]
, we can then call updateStateFunction('newValue')
, and currentState
will be updated to equal 'newValue'
on the next render.
Next in line we have useEffect, which is basically a function that says run this whenever any value in the dependency array change.
useEffect(() => {
localStorage.setItem('dark', JSON.stringify(isDarkMode))
}, [isDarkMode])
What we are saying with the above code is: When isDarkMode
changes, set the localStorage
value to the new isDarkMode
value. This ensures that the user preference is always updated.
Lastly we have the Context.Provider
itself. As its name implies, it is used to provide Context.Consumer
’s with a value. <ThemeModeContext.Consumer>
and useContext(ThemeModeContext)
will only have a value if the the provider is present in the tree above the consumer. ThemeProvider
is used to enable custom styled-components themes and mostly works like any other context.
<ThemeModeContext.Provider value={mode}>
<ThemeProvider theme={mode.theme}>{children}</ThemeProvider>
</ThemeModeContext.Provider>
Now that you have a better understanding of what the provider does, you need to encapsulate the root of your application with the ThemeModeProvider
component. In a regular create-react-app app, it will be something like this:
ReactDOM.render(
<ThemeModeProvider>
<App />
</ThemeModeProvider>,
document.getElementById('root')
)
Consuming the Context
In order to actually use the context in one of your components, you can either use Context.Consumer
component or useContext
hook, I will only show how to use the hook implementation as it is much simpler and more straightforward.
Anywhere you want to be able to change the current theme, be it a button
or a toggle
like in the top of this page, just call useContext(ThemeModeContext)
and you will have access to the setIsDarkMode
function.
import React, { useContext } from 'react'
import { ThemeModeContext } from './theme'
export const Buttons = () => {
const { setIsDarkMode } = useContext(ThemeModeContext)
const setDark = () => setIsDarkMode(true)
const setLight = () => setIsDarkMode(false)
const toggleMode = () => setIsDarkMode((s) => !s)
return (
<>
<button onClick={setDark}>set dark</button>
<button onClick={setLight}>set light</button>
<button onClick={toggleMode}>toggle</button>
</>
)
}
These are all the parts needed to create a fairly simple dark mode toggle. You can, of course, improve and simplify steps as you see fit. To check an actual usage of the code you can check the Code Sandbox bellow.