How to Code a Dark Theme with Material UI
Tags: gatsby, react, design, webdev
Published: 2021-06-15 23:43:56

__Dark mode__ is a feature that users [can't get enough of](https://www.searchenginewatch.com/2020/09/30/why-dark-mode-web-designs-are-gaining-popularity/ "external link to Article on Rising Popularity of Dark Modes"). It saves battery life, reduces eye strain, and minimizes blue light emissions. It's a simple feature that as a developer (all else equal), will set you apart far and wide from your competition. To boot, [Material-UI](https://material-ui.com/getting-started/installation/ "external link to Material-UI's Get Started Information") supports dark/light themes out of the box, making it a great framework to build on. Despite this, due to dark mode's relative infancy in the web development world, there is a distinct lack of documentation and tutorials on how to *actually* code dark & light modes.  
  
*This article was originally published on [Snappy Web Design](https://snappywebdesign.net/blog/how-to-code-a-dark-theme-with-material-ui "external link to Snappy Web Design's Blog")*  

## In this Material-UI tutorial, you'll learn  

- How to use localStorage to save a user's theme preference
- How to use Material-UI to apply a dark theme and light theme
- How to use [Gatsby's](https://www.gatsbyjs.com/docs/reference/config-files/ "external link to Gatsby's Documentation on Config Files ") `gatsby-browser` and `gatsby-ssr` to avoid css style conflicts on rehydration with server side rendering (SSR) 
- How to use a single Mui Theme file to serve both dark/light theme variants ("[single source of truth](https://en.wikipedia.org/wiki/Single_source_of_truth "external link to Wikipedia article on Single Source of Truth")")
- How to use [React's](https://reactjs.org/docs/hooks-reference.html "external link to React.js's Documentation page on Hooks API Reference") `useReducer`, `useContext`, `createContext`, and `Context.Provider`

### Why this Tutorial?
Although there are other tutorials on the web and the documentation for Material-UI is normally stout, you've probably found while researching tutorials on dark modes:
- Most tutorials show impractical / unorderly code that's difficult to reuse in your own project
- Material-UI's documentation falls short of demonstrating how to update the theme live - it only briefly touches on 'dark' and 'light' theme types
- Incomplete examples lead to __Flashes of Unstyled Content__ ([FOUC](https://en.wikipedia.org/wiki/Flash_of_unstyled_content "external link to Wikipedia article on Flash of Unstyled Content"))
- Gatsby's __Server Side Rendering__ ([SSR](https://www.gatsbyjs.com/docs/glossary/server-side-rendering/ "external link to Gatsby's article on Server Side Rendering")) leads to FOUC

### What's the finished product?
You can view the final code here:   

__[Live Deployed Site](https://mui-dark-mode.vercel.app/ "external link to Deployed Site on Vercel")__  

__[View on CodeSandbox](https://codesandbox.io/s/material-ui-dark-mode-with-gatsby-and-usereducer-cr5ml?file=/src/pages/index.js "external link to Live CodeSandbox Demonstration of MUI Light and Dark Modes ")__   

__[View the Github Repo](https://github.com/SnappyWebDesign/material-ui-dark-mode "external link to Snappy Web Design's Github repo material-ui-dark-mode")__

...and here's how the final product will look and behave:  

![2021-06-15 15-17-41 3](//images.ctfassets.net/z7pyyvho21dm/BGict8bOTyxk77VE629Kr/13a4d9d7344a18a62d183441c4092c3c/2021-06-15_15-17-41_3.gif)

### Project Structure
Before we dive into the code, let's first look at the project structure (which is available on CodeSandbox).  

![filestructure](//images.ctfassets.net/z7pyyvho21dm/334oLglfK6VNU3dEsHYCFW/5ed8dbcd00a3ed36c278a8711f5f5f27/filestructure.jpg)

You'll notice it looks similar to a typical Gatsby.js project with the exception of the `ThemeHandler.js` file. 

__ThemeHandler__ will...well, handle whether to display a light or dark theme. It'll contain our useContext and useReducer functions.

__gatsby-browser__ wraps our application with our Context Provider. It allows our Gatsby site to have dynamic state.

__gatsby-ssr__ serves the same purpose: wrapping our application with our Context Provider to make it accessible everywhere in our app. It prevents __flashes of unstyled content__ with server-side rendering.

__Layout__ is where we'll initially check the user's local storage to see if they have a previously set theme. If not, we'll set it to the default of our choosing. We'll wrap our application with our Theme using the [Material-UI ThemeProvider.](https://material-ui.com/styles/api/#themeprovider "external link to Material-UI's ThemeProvider Documentation")

__Index__ does the least amount of work but the most important. It contains the button to toggle the dark/light theme and does so with an onClick function. This dispatches a function via our reducer to change the theme and sets the local storage to the user's newly-preferred theme.

__Theme__ contains our:   
__1. Base theme__, styles to be applied globally across both light *and* dark modes.   
__2. Dark theme__, styles applied when dark mode is active, and lastly, our   
__3. Light theme__, containing styles to be applied when the light mode is active.

![Untitled Diagram](//images.ctfassets.net/z7pyyvho21dm/4E2usnlx5XXU2uJBRrqj04/6457dd94a150ed34290c20169b3cfe98/Untitled_Diagram.jpg)

If you're a visual learner, I hope that diagram gives you a mental picture of where we're headed.

---

### Theme.js
One of the reasons why I think this approach is the best is because it has a *single source of truth.* Unlike other tutorials, we only use __one theme__, yet we provide multiple styles. We do it by nesting our themes: we define our global styles for both light and dark modes, and then spread that across our styles for our separate light and dark themes.

```javascript{1-36}
import { createMuiTheme } from "@material-ui/core/styles"

const baseTheme = createMuiTheme({
  typography: {
    fontFamily: "'Work Sans', sans-serif",
    fontSize: 14,
    fontFamilySecondary: "'Roboto Condensed', sans-serif"
  }
})

const darkTheme = createMuiTheme({
  ...baseTheme,
  palette: {
    type: "dark",
    primary: {
      main: "#26a27b"
    },
    secondary: {
      main: "#fafafa"
    }
  }
})
const lightTheme = createMuiTheme({
  ...baseTheme,
  palette: {
    type: "light",
    primary: {
      main: "#fafafa"
    },
    secondary: {
      main: "#26a27b"
    }
  }
})

export { darkTheme, lightTheme }
```

Now our theme is set up for us to later import it like  
`import { darkTheme, lightTheme } from "./Theme"`  
Eventually, we'll make use of Material-UI's theme provider and pass in our theme dynamically:  
`<ThemeProvider theme={darkMode ? darkTheme : lightTheme}>`  
  
For now though, let's work on our ThemeHandler.


---

### ThemeHandler.js
Our objective is simple: create a state value for `darkMode`, set it false initially, and be able to access and update our state from anywhere within our Gatsby application.  

For this, we make use of React's createContext, useReducer, and ContextProvider.  
  
First up, we need to import `createContext` and `useReducer`, assign a variable as our action type which we'll use in our Reducer, and initialize our new Context:  
```javascript{1-4}
import React, { createContext, useReducer } from "react"
let SET_THEME

export const darkModeContext = createContext()
```  
Then, we'll create our __useReducer__ function. Essentially, we'll be calling a function to set darkMode either true or false. The reducer is a switch statement to feed this value to our global state.  
```javascript{6-16}
import React, { createContext, useReducer } from "react"
let SET_THEME

export const darkModeContext = createContext()

export const darkModeReducer = (state, action) => {
  switch (action.type) {
    case SET_THEME:
      return {
        ...state,
        darkMode: action.payload
      }
    default:
      return state
  }
}
```  
Then, we'll create and export our DarkModeState function. We'll set our initial state (set dark mode to false on first load) in addition to initializing our dispatch function using the reducer we just created.  
```javascript{18-22}
import React, { createContext, useReducer } from "react"
let SET_THEME

export const darkModeContext = createContext()

export const darkModeReducer = (state, action) => {
  switch (action.type) {
    case SET_THEME:
      return {
        ...state,
        darkMode: action.payload
      }
    default:
      return state
  }
}

export const DarkModeState = props => {
  const initialState = {
    darkMode: "false"
  }
  const [state, dispatch] = useReducer(darkModeReducer, initialState)
```  
Lastly, we'll create our function (`setDarkMode`) to update our state. It uses the dispatch function which feeds into our reducer's switch statement.  
We return our `darkModeContext.Provider` which makes both the __darkMode__ state, and the __setDarkMode__ function available globally across our app.  
```javascript{24-40}
import React, { createContext, useReducer } from "react"
let SET_THEME

export const darkModeContext = createContext()

export const darkModeReducer = (state, action) => {
  switch (action.type) {
    case SET_THEME:
      return {
        ...state,
        darkMode: action.payload
      }
    default:
      return state
  }
}

export const DarkModeState = props => {
  const initialState = {
    darkMode: "false"
  }
  const [state, dispatch] = useReducer(darkModeReducer, initialState)

  const setDarkMode = async bool => {
    dispatch({
      type: SET_THEME,
      payload: bool
    })
  }

  return (
    <darkModeContext.Provider
      value={{
        darkMode: state.darkMode,
        setDarkMode
      }}
    >
      {props.children}
    </darkModeContext.Provider>
  )
}
```  


---

### 🔧 Fixing Gatsby's Rehydration Issue  
__WARNING:__ Do not skip this step or you will waste hours of your life debugging. I wasted two days debugging flashes of unstyled content the first time I implemented dark mode - learn from my mistakes.  

Because Gatsby builds pages long before they're rendered and served to the end-user's web browser, we have to take a couple additional steps when using dynamic state values.   

If you want to read more about server-side rendering and Gatsby's webpack -- be my guest. In fact, you probably should read about Gatsby's [Browser APIs](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/ "external link to Gatsby's Documentation on Browser APIs"). But for sake of brevity, let me sum it up like this:  
  
You need to wrap every page with your React.useState component in Gatsby. Luckily, we can use Gatsby's built in API via the `gatsby-browser.js` and `gatsby-ssr.js` files. The syntax and content of the files are the exact same:  
  
__gatsby-browser.js__  
```javascript{1-6}
import React from "react"
import { DarkModeState } from "./src/components/UI/ThemeHandler"

export function wrapRootElement({ element, props }) {
  return <DarkModeState {...props}>{element}</DarkModeState>
}
```  
__gatsby-ssr.js__  
```javascript{1-6}
import React from "react"
import { DarkModeState } from "./src/components/UI/ThemeHandler"

export function wrapRootElement({ element, props }) {
  return <DarkModeState {...props}>{element}</DarkModeState>
}
```  


---

### Layout.js  
We're almost to the end! The Layout provides our styles to the rest of our app via [Material-UI's ThemeProvider.](https://material-ui.com/styles/api/#themeprovider "external link to Material-UI's ThemeProvider Documentation"). Our approach (from a high-level) is:  
1. Import our light/dark themes
2. Import our theme handler (`darkModeContext`)
3. Check the users localStorage to see if a preferred theme is already set in a `useEffect` function
4. If not, set the users preferred theme to the default (darkMode: false)
5. Wrap our component with our dynamic theme (either light or dark) via the `ThemeProvider`

Importantly, we need to also import and include the `<CssBaseline />` component from Material-UI for the ThemeProvider to work.  
  
The code for this is hardly worth elaborating on, so I'll let it speak for itself:  
```javascript{1-32}
import React, { useContext, useEffect } from "react"
import CssBaseline from "@material-ui/core/CssBaseline"
import { ThemeProvider } from "@material-ui/core/styles"
import { darkTheme, lightTheme } from "./Theme"
import { darkModeContext } from "./ThemeHandler"

const Layout = ({ children }) => {
  const DarkModeContext = useContext(darkModeContext)
  const { darkMode, setDarkMode } = DarkModeContext

  useEffect(() => {
    const theme = localStorage.getItem("preferred-theme")
    if (theme) {
      const themePreference = localStorage.getItem("preferred-theme")
      if (themePreference === "dark") {
        setDarkMode(true)
      } else {
        setDarkMode(false)
      }
    } else {
      localStorage.setItem("preferred-theme", "light")
      setDarkMode(true)
    }
  }, [])

  return (
    <ThemeProvider theme={darkMode ? darkTheme : lightTheme}>
      <CssBaseline />
      <main>{children}</main>
    </ThemeProvider>
  )
}
```  


---

### Index.js (The final step!)
If you've made it this far, pat yourself on the back. This is the final (and simplest) step before you'll have a functioning dark mode toggle.   

Let's not waste any more time.  

1. First, we need to wrap our Index Page with our Layout component.
2. Then, we need to create a button to toggle the theme
3. We need to create an onClick function for the button, `handleThemeChange`
4. Inside the function, we update `localStorage` and `setDarkMode` either true or false using our Context Provider:  
```javascript{1-34}
import React, { useContext } from "react"
import Layout from "../components/UI/Layout"
import Button from "@material-ui/core/Button"
import { darkModeContext } from "../components/UI/ThemeHandler"

const IndexPage = () => {
  const DarkModeContext = useContext(darkModeContext)
  const { darkMode, setDarkMode } = DarkModeContext

  const handleThemeChange = () => {
    if (darkMode) {
      localStorage.setItem("preferred-theme", "light")
      setDarkMode(false)
    } else {
      localStorage.setItem("preferred-theme", "dark")
      setDarkMode(true)
    }
  }

  return (
    <Layout>
        <Button
          variant="contained"
          color="secondary"
          size="medium"
          onClick={handleThemeChange}
        >
          Toggle {darkMode ? "Light" : "Dark"} Theme
        </Button>
    </Layout>
  )
}

export default IndexPage
```  
__Boom! Just like that, you have a toggleable dark/light mode with Gatsby and Material-UI.__


---

### Finished Product 
<iframe src="https://codesandbox.io/embed/material-ui-dark-mode-with-gatsby-and-usereducer-forked-itdwv?fontsize=12&hidenavigation=1&theme=light"
     style="width:100%; height:500px; border:0; border-radius: 4px; overflow:hidden;"
     title="Material-UI Dark Mode with Gatsby and useReducer (forked)"
     allow="accelerometer; ambient-light-sensor; camera; encrypted-media; geolocation; gyroscope; hid; microphone; midi; payment; usb; vr; xr-spatial-tracking"
     sandbox="allow-forms allow-modals allow-popups allow-presentation allow-same-origin allow-scripts"
   ></iframe>
  
__[Live Deployed Site](https://mui-dark-mode.vercel.app/ "external link to Deployed Site on Vercel")__  

__[View on CodeSandbox](https://codesandbox.io/s/material-ui-dark-mode-with-gatsby-and-usereducer-cr5ml?file=/src/pages/index.js "external link to Live CodeSandbox Demonstration of MUI Light and Dark Modes ")__   

__[View the Github Repo](https://github.com/SnappyWebDesign/material-ui-dark-mode "external link to Snappy Web Design's Github repo material-ui-dark-mode")__

---

### Did you find this article helpful?  
If you read this whole article, __thank you__. I hope you learned something valuable.  
  
If you did, would you take a second to share the article by clicking below? It helps our cause immensely!  

Make sure to also click the follow button to get notified when new posts go live 🔔