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!
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;