Material UI Dark Mode Theme and Toggle

In this example, we have implemented a dark mode switch in the App Bar, defaulted to dark mode being toggled on. This sets our theme to be dark for our whole Material UI app.

Accomplishing this is quite complex, as we've introduced custom React context and hooks. Observe the ThemeProvider in App.js. See that we are passing in muiTheme. muiTheme is now retrieved from a createTheme call (from Mui), with our own themeOverrides.

On the line prior to that, we get themeOverrides from the result of a new custom hook we added called useTheme. Head on over to Theme.js to see how this works.

Theme.js exports a custom useTheme hook, which initializes a set of themeOverrides. It gets these overrides by default from getDefaultTheme, which will actually check localStorage to see if we have any overrides set (this is to allow saving our dark mode preference in local storage). If nothing is found in localStorage, it will use the freshDefaultTheme function to generate these overrides here in this file. These are overrides for the specific "mode" (dark or light).

Diving into the freshDefaultTheme function, we set palette's mode attribute to be whatever mode we are in, light or dark. This does most of the work for us. But, we still need to do more overrides, in particular the MuiCard needs to have its own overrides. We can restyle the cards globally in through styleOverrides: { root: { backgroundColor attributes. What we end up with here is a set of overrides for light theme or dark theme. These are then stored in local storage via the useEffect hook in this custom hook. Most importantly, setPaletteMode is the toggle function that's called when someone toggles dark or light mode on. In setPaletteMode, we call setThemeOverrides, which will call our freshDefaultTheme with the new mode.

The result of the useTheme hook is that it returns themeOverrides, setThemeOverrides, and setPaletteMode. setPaletteMode is used in DarkSwitch.js (you may need to scroll right to find this file), which is rendered inside the AppBar. In DarkSwitch.js, we useContext for our CustomThemeContext. We can't just call useTheme here, because that hook is only called at the app level (because it is the only hook responsible for managing the state of the theme overrides). The result of the hook is actually what's passed directly into the context.

So we retrieve setPaletteMode/themeOverrides and use those to determine if the switch is dark or light (themeOverrides.palette.mode === "dark") and when the switch is set, we call setPaletteMode with whether the result of the switch was checked.

We told you this was complicated!

Menu
Lesson 5/6
import React, { useState, useEffect, createContext } from "react";
import { createTheme, ThemeProvider } from "@mui/material/styles";
import CssBaseline from "@mui/material/CssBaseline";
import Box from "@mui/material/Box";

import AppBar from "./AppBar";
import Drawer from "./Drawer";
import MainContent from "./MainContent";
import { useTheme } from "./Theme";

export const drawerWidth = 240;
export const CustomThemeContext = createContext();

function App() {
  const [open, setOpen] = useState(true);
  const themeContext = useTheme();
  const { themeOverrides } = themeContext;
  const [muiTheme, setMuiTheme] = useState(createTheme(themeOverrides));

  useEffect(() => {
    setMuiTheme(createTheme(themeOverrides));
  }, [themeOverrides]);

  const toggleDrawer = () => {
    setOpen(!open);
  };

  return (
    <CustomThemeContext.Provider value={themeContext}>
      <ThemeProvider theme={muiTheme}>
        <Box sx={{ display: "flex" }}>
          <CssBaseline />
          <AppBar toggleDrawer={toggleDrawer} open={open} />
          <Drawer toggleDrawer={toggleDrawer} open={open} />
          <MainContent />
        </Box>
      </ThemeProvider>
    </CustomThemeContext.Provider>
  );
}

export default App;