How to Code a Dark Theme with Material UI

Turn your Gatsby website from light mode to dark mode with Material-UI

Profile Pic of Snappy Web Design

Jun 15 · 12 min read

How to Code a Dark Theme with Material UI

Dark mode is a feature that users can't get enough of. 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 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.

View article on Medium

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 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")
  • How to use React's 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)
  • Gatsby's Server Side Rendering (SSR) leads to FOUC

What's the finished product?

You can view the final code here:

Live Deployed Site

View on CodeSandbox

View the Github Repo

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

2021-06-15 15-17-41 3

Project Structure

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

filestructure

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.

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

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.

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:

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.

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.

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.

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. 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

import React from "react" import { DarkModeState } from "./src/components/UI/ThemeHandler" export function wrapRootElement({ element, props }) { return <DarkModeState {...props}>{element}</DarkModeState> }

gatsby-ssr.js

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.. 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:

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:
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

Live Deployed Site

View on CodeSandbox

View the Github Repo


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 🔔

Hire me

Most popular posts

1.

How to Code a Dark Theme with Material UI

2.

Gatsby Code Splitting

3.

Michigan Website Design

4.

The Top Web Design Features Small Business Owners Need to Succeed Online

For Developers

1.

How to make a Font Size Slider in React with Material UI

2.

How to Add Structured Data to Blog Posts in Gatsby

3.

How to Add Breadcrumbs to Google Search in Gatsby

For Business Owners

1.

What the heck is Gatsby js and why use it

2.

10 Landing Page Designs to Inspire Your Small Business

3.

About Google Page Speed Scores and Why They Matter

More from Snappy Web Design

Subscribe to the Snappy Web Design Blog to get notified when a new post goes live. We'll never share your e-mail address or use it for any reason other than to share new posts.

Published May 5

Discover the secrets to designing a website that speaks to your small business's unique identity

As a website developer in Michigan, I’ve worked with many small business owners looking to redesign their websites to increase sales and connect with new and existing customers. As such, I’ve seen what works and what doesn’t work and cultivated a list of valuable tips that I give to all my clients.

As a small business owner in Michigan, your website is frequently the first impression that customers have of your company. It’s essential that your website is not only visually appealing an...


Published September 1

Snappy Web Design is the one-stop-shop for web design.

Michigan's Newest Web Design Company

You search Google for “Michigan web design” and find my site. I’m Joe, and I’m the head developer at Snappy Web Design. I’m the new guy on the block, and an expert in providing full-service web design for small business owners in MI.

I’m different from typical web design agencies because I aim to be your partner for all things related to your onli...


Copyright © 2023. Snappy Web Design. All rights reserved.