I recently implemented Dark Mode on this website. Let me talk you through how I went about it.
prefers-color-scheme
and apply custom styling respectively.localStorage
.<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
</body>
</html>
// App.jsx
import * as React from 'react'
const App = () => 'Hello world'
export default App
// index.jsx
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App />, document.getElementById('app'))
We will use CSS custom properties here. The key advantage to this is that we don't have to redeclare CSS properties for light mode and dark mode. For example:
/* Bad */
body {
background: #fff;
}
body.dark-mode {
background: #000;
}
Instead, we can do something like this:
/* Nice */
:root {
--bg: #fff;
}
.dark-mode {
--bg: #000;
}
body {
background-color: var(--bg);
}
This allows us to separate the concerns of the theme from other aspects of our CSS selectors with the ability to even extract and maintain the colours in the theme in a separate file altogether. If you're using something like SCSS
, we can easily leverage something like @import
to achieve modularity and organisation of our styles.
For now though, let's just inline the styles in our index.html
.
<html>
<head>
<style>
:root {
--bg: #fff;
--text: #000;
}
.dark-mode {
/* add dark mode styles */
--bg: #000;
--text: #fff;
}
body {
background-color: var(--bg);
color: var(--text);
}
</style>
</head>
<body>
<div id="app"></div>
<script src="./index.jsx"></script>
</body>
</html>
The way this will work is we want to add a dark-mode
CSS class to the body
of the document.
We want this class to be added as early as possible. We could add it at the very top level of our component tree; in our case; App.jsx - however doing this would mean that the React app would still have to start before we would know the user's preference or be able to set the theme. This would result in a potential "flash" of an undesired theme (or FART 😁).
In order to get around this, let's edit index.html
and add a function to detect and set dark mode before our React app starts:
<html>
<head>
<style>
:root {
--bg: #fff;
--text: #000;
}
.dark-mode {
/* add dark mode styles */
--bg: #000;
--text: #fff;
}
body {
background-color: var(--bg);
color: var(--text);
}
</style>
</head>
<body>
<div id="app"></div>
<script>
function prefersDarkMode() {
if (typeof window !== 'undefined') {
if (localStorage.getItem('prefersDarkMode')) {
return localStorage.getItem('prefersDarkMode') === 'true'
}
return (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches
)
}
}
;(() => {
document.body.classList.toggle('dark-mode', prefersDarkMode())
})()
</script>
<script src="./index.jsx"></script>
</body>
</html>
This will first check localStorage
if this has already been stored (returning visitor) and if not, we will default to their device setting using prefers-color-scheme
.
I won't go through the styling aspect of the switch as you can find many toggle/on-off switch styles on Codepen etc. I will show you how I got this functional though. The switch is based on a styled checkbox
so all we need to be concerned about is the onChange
event.
import * as React from 'react'
const DarkModeToggle = () => {
const [isDarkMode, setIsDarkMode] = React.useState(false)
React.useEffect(() => {
setIsDarkMode(document.body.classList.contains('dark-mode'))
}, [])
const toggle = () => {
setIsDarkMode(!isDarkMode)
document.body.classList.toggle('dark-mode', !isDarkMode)
}
return (
<div className="dark-mode-toggle">
<input type="checkbox" checked={isDarkMode} onChange={toggle} />
</div>
)
}
export default DarkModeToggle
Quite a simple component. It does a few things.
body
element and checks if the dark-mode
class is presentdark-mode
CSS classname on the body
elementYou should now be at a point where you have a functional toggle switch that switches dark mode on and off.
Luckily, we don't have to add much to accomplish this. We can simply update our toggle
function like below:
const toggle = () => {
setIsDarkMode(!isDarkMode)
document.body.classList.toggle('dark-mode', !isDarkMode)
+ window.localStorage.setItem('prefersDarkMode', String(!isDarkMode))
}
import * as React from 'react'
import DarkModeToggle from './DarkModeToggle'
const App = () => (
<>
<p>Hello world</p>
<DarkModeToggle />
</>
)
export default App
And that's it!
It's worth noting that items set in localStorage
don't have any expiry like cookies. If you want to implement something that expires, you can switch out this implementation with cookies instead. Alternatively, you could use sessionStorage
but that only lasts as long the tab or window is open.
Personally, I felt that localStorage
was the best solution as the preference is remembered indefinitely unless the user decides to change it.