d
Amit DhamuSoftware Engineer
 

Implementing Dark Mode

9 minute read 00000 views

I recently implemented Dark Mode on this website. Let me talk you through how I went about it.

Acceptance Criteria

  1. Check the user's device setting for prefers-color-scheme and apply custom styling respectively.
  2. Have a toggle switch that allows users to override this and switch to either dark or light mode.
  3. If they have overidden the device setting, store this preference in localStorage.

Setup React app

<!-- 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'))

Create the CSS theme

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>

Check if Dark Mode is Enabled

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.

Create a toggle switch

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.

  • Uses local state for setting dark mode
  • Looks at the body element and checks if the dark-mode class is present
  • Clicking the checkbox:
    • Sets dark mode to the opposite of what is set in local state (toggling it)
    • Toggles the dark-mode CSS classname on the body element

You should now be at a point where you have a functional toggle switch that switches dark mode on and off.

Storing the result of a toggle to Local Storage

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

Add the component to our App

import * as React from 'react'

import DarkModeToggle from './DarkModeToggle'

const App = () => (
  <>
    <p>Hello world</p>
    <DarkModeToggle />
  </>
)

export default App

And that's it!

Demo



Final Thoughts

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.