Reputation:
I am using mui5(Material Ui)
in my nextjs
application. I am trying to implementing dark mode
. All are going well. I want a feature that if any user toggle the dark mode then it will be saved in local-storage. Then when I refresh the page, It automatically getting value from local-storage and active dark or light mode according to value from local-storage. If user first come to the site then it should active automatically system preference mode. I mean If there are no value in the local-storage then it should active automatically system preference mode. How can I do that.
Here is my code-
_app.js
export default function MyApp(props) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props;
const [mode, setMode] = React.useState("light");
const colorMode = React.useMemo(
() => ({
// The dark mode switch would invoke this method
toggleColorMode: () => {
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
}),
[],
);
// Update the theme only if the mode changes
const muiTheme = React.useMemo(() => createTheme(theme(mode)), [mode]);
return (
<ColorModeContext.Provider value={colorMode}>
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider theme={muiTheme}>
<CssBaseline />
<Component {...pageProps} />
</ThemeProvider>
</CacheProvider>
</ColorModeContext.Provider>
);
}
MyApp.propTypes = {
Component: PropTypes.elementType.isRequired,
emotionCache: PropTypes.object,
pageProps: PropTypes.object.isRequired,
};
Toogle button-
const theme = useTheme();
const colorMode = useContext(ColorModeContext);
<FormControlLabel
control={<MaterialUISwitch sx={{ m: 1 }} checked={theme.palette.mode === 'dark' ? false : true} />}
label=""
sx={{ mx: "0px" }}
onClick={colorMode.toggleColorMode}
/>
Upvotes: 3
Views: 5239
Reputation: 31
It's currently still an experimental API but MUI has fixed this with their implementation of CSS Theme variables.
From the Migration guide :
Replace ThemeProvider
with CssVarsProvider
import { Experimental_CssVarsProvider as CssVarsProvider } from '@mui/material/styles';
function App() {
return <CssVarsProvider>...</CssVarsProvider>;
}
Then replace your old themes created with createTheme
with extendTheme
:
import { experimental_extendTheme as extendTheme} from '@mui/material/styles';
const theme = extendTheme({
colorSchemes: {
light: {
palette: {
primary: {
main: '#ff5252',
},
...
},
},
dark: {
palette: {
primary: {
main: '#000',
},
...
},
},
},
// ...other properties
});
Toggle and localstorage logic can be removed, MUI handles this now with the useColorScheme
Hook.
import {
Experimental_CssVarsProvider as CssVarsProvider,
experimental_extendTheme as extendTheme,
useColorScheme,
} from '@mui/material/styles';
function ModeToggle() {
const { mode, setMode } = useColorScheme();
return (
<Button
onClick={() => {
setMode(mode === 'light' ? 'dark' : 'light');
}}
>
{mode === 'light' ? 'Turn dark' : 'Turn light'}
</Button>
);
}
const theme = extendTheme({
// ...your custom theme
});
function App() {
return (
<CssVarsProvider theme={theme}>
<ModeToggle />
...
</CssVarsProvider>
);
}
To prevent the flicker just place getInitColorSchemeScript()
before <Main />
in your pages/_document.js
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { getInitColorSchemeScript } from '@mui/material/styles';
export default class MyDocument extends Document {
render() {
return (
<Html>
<Head>...</Head>
<body>
{getInitColorSchemeScript()}
<Main />
<NextScript />
</body>
</Html>
);
}
}
Upvotes: 3
Reputation: 393
Here is how I implemented dark mode in React MUI 5. I made a custom hook and fully supported three modes: "light," "dark," and "system."
Hope this helps someone looking for a similar answer. :)
// ColorModeContext.js
import React from "react";
export const ColorModeContext = React.createContext({
colorMode: "system",
toggleColorMode: () => {},
});
// useThemeSwitcher.js
import React from "react";
import { createTheme, useMediaQuery } from "@mui/material";
import theme from "configs/Theme";
import { Light, Dark } from "configs/Palette";
const useThemeSwitcher = () => {
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)") ? "dark" : "light"; // Get user browser scheme
const [mode, setMode] = React.useState(localStorage.getItem("theme") || prefersDarkMode);
const colorMode = React.useMemo(
() => ({
toggleColorMode: (selected) => {
setMode(selected === "system" ? prefersDarkMode : selected);
},
}),
[prefersDarkMode]
);
const newTheme = React.useMemo(() => {
const themeMode = localStorage.getItem("theme") === "system" ? prefersDarkMode : mode;
return createTheme({
...theme,
palette: {
mode: themeMode,
...(themeMode === "dark" ? Dark : Light), // Your custom light and dark palette
},
});
}, [mode, prefersDarkMode]);
return { newTheme, colorMode };
};
export default useThemeSwitcher;
// App.js
import React from "react";
import CssBaseline from "@mui/material/CssBaseline";
import { ThemeProvider, StyledEngineProvider } from "@mui/material/styles";
import Root from "./Root";
import { ColorModeContext } from "ColorModeContext";
import useThemeSwitcher from "hooks/useThemeSwitcher";
const App = () => {
const { newTheme, colorMode } = useThemeSwitcher();
return (
/* Provide Redux store */
<StyledEngineProvider injectFirst>
<ColorModeContext.Provider value={colorMode}>
<ThemeProvider theme={newTheme}>
<CssBaseline />
<Root />
</ThemeProvider>
</ColorModeContext.Provider>
</StyledEngineProvider>
);
};
export default App;
// ThemeSwitcher.js
import React from "react";
import { Divider, MenuItem } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import Brightness4Icon from "@mui/icons-material/Brightness4";
import Brightness7Icon from "@mui/icons-material/Brightness7";
import { ColorModeContext } from "ColorModeContext";
import Popover from '@mui/material/Popover';
const ThemeSwitcher = () => {
const theme = useTheme();
const { toggleColorMode } = React.useContext(ColorModeContext);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
const switchMode = (mode) => {
toggleColorMode(mode);
localStorage.setItem("theme", mode);
};
React.useEffect(() => {
if (!localStorage.getItem("theme")) {
localStorage.setItem("theme", "system");
}
}, []);
return (
<>
<Button aria-describedby={id} variant="contained" onClick={handleClick}>
theme.palette.mode === "dark" ? (
<Brightness7Icon sx={{ fill: "#fff" }} />
) : (
<Brightness4Icon sx={{ fill: "#000" }} />
)Ï
</Button>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<MenuItem onClick={() => switchMode("light")}>Light</MenuItem>
<Divider sx={{ mb: "0.5rem", borderBottom: "thin dashed rgba(145, 158, 171, 0.24)" }} />
<MenuItem onClick={() => switchMode("dark")}>Dark</MenuItem>
<Divider sx={{ mt: "0.5rem", borderBottom: "thin dashed rgba(145, 158, 171, 0.24)" }} />
<MenuItem onClick={() => switchMode("system")}>System</MenuItem>
</Popover>
</>
);
};
export default ThemeSwitcher;
Upvotes: 0
Reputation: 1303
I followed the answer of @giorgiline and everything was fine till the local storage part, which for me seems to be a bit too complicated.
What I did instead is in the ThemeContext.tsx
looks like this:
This bit is different:
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
// reading the storage
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
// setting the storage
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
Here the whole file
import {createContext, ReactNode, useContext, useEffect, useState} from 'react'
import {createTheme, ThemeProvider as MuiThemeProvider} from '@mui/material'
import lightTheme from "../styles/theme/lightTheme";
import hightContrastTheme from "../styles/theme/hightContrastTheme";
import theme from "@storybook/addon-interactions/dist/ts3.9/theme";
interface ThemeContextType {
themeMode: string
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const [themeMode, setThemeMode] = useState("light");
useEffect(() => {
const stored = localStorage.getItem("theme");
setThemeMode(stored ? JSON.parse(stored) : "light");
}, []);
function updateTheme(theme: string){
setThemeMode(theme)
localStorage.setItem("theme", JSON.stringify(theme));
}
const toggleTheme = () => {
switch (themeMode) {
case 'light':
updateTheme("highContrast")
break
case 'highContrast':
updateTheme("light")
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? createTheme(lightTheme) : createTheme(hightContrastTheme)}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
Upvotes: 0
Reputation: 1371
Here you can have an example of what I've done with Next.js and Material UI (5) to:
I'm using Typescript, but it doesn't matter if you use plain JavaScript
We'll have 2 files to modify the specific properties independently.
darkTheme.ts
import { createTheme } from '@mui/material/styles'
const darkTheme = createTheme({
palette: {
mode: 'dark',
},
})
export default darkTheme
lightTheme.ts
import { createTheme } from '@mui/material/styles'
const lightTheme = createTheme({
palette: {
mode: 'light',
},
})
export default lightTheme
The important thing here is the value and function retrieved from the context, the button style or icon can be anything.
interface ThemeSwitcherButtonProps extends IconButtonProps { }
const ThemeSwitcherButton = ({ ...rest }: ThemeSwitcherButtonProps) => {
const { themeMode, toggleTheme } = useThemeContext()
return (
<Tooltip
title={themeMode === 'light' ? `Switch to dark mode` : `Switch to light mode`}
>
<IconButton
{...rest}
onClick={toggleTheme}
>
{themeMode === 'light' ? <DarkModeOutlined /> : <LightModeRounded />}
</IconButton>
</Tooltip>
)
}
export default ThemeSwitcherButton
We use useMediaQuery
from material ui library to check the preference mode of the browser. This hook works with client rendering and ssr.
We also use useLocalStorage
hook to save the state in the local storage so it's persisted.
We wrap the original Material UI ThemeProvider (renamed as MuiThemeProvider) with this new Provider, so then in the _app file only one Provider is needed
ThemeContext.tsx
import { createContext, ReactNode, useContext } from 'react'
import { ThemeProvider as MuiThemeProvider, useMediaQuery } from '@mui/material'
import lightTheme from '@/themes/light'
import darkTheme from '@/themes/dark'
import useLocalStorage from '@/react/hooks/useLocalStorage'
const DARK_SCHEME_QUERY = '(prefers-color-scheme: dark)'
type ThemeMode = 'light' | 'dark'
interface ThemeContextType {
themeMode: ThemeMode
toggleTheme: () => void
}
const ThemeContext = createContext<ThemeContextType>({} as ThemeContextType)
const useThemeContext = () => useContext(ThemeContext)
const ThemeProvider = ({ children }: { children: ReactNode }) => {
const isDarkOS = useMediaQuery(DARK_SCHEME_QUERY)
const [themeMode, setThemeMode] = useLocalStorage<ThemeMode>('themeMode', isDarkOS ? 'light' : 'dark')
const toggleTheme = () => {
switch (themeMode) {
case 'light':
setThemeMode('dark')
break
case 'dark':
setThemeMode('light')
break
default:
}
}
return (
<ThemeContext.Provider value={{ themeMode, toggleTheme }}>
<MuiThemeProvider theme={themeMode === 'light' ? lightTheme : darkTheme}>
{children}
</MuiThemeProvider>
</ThemeContext.Provider>
)
}
export {
useThemeContext,
ThemeProvider
}
_app.tsx
export default function MyApp(props: MyAppProps) {
const { Component, emotionCache = clientSideEmotionCache, pageProps } = props
const getLayout = Component.getLayout ?? ((page) => page)
return (
<CacheProvider value={emotionCache}>
<Head>
<meta name="viewport" content="initial-scale=1, width=device-width" />
</Head>
<ThemeProvider>
{/* CssBaseline kickstart an elegant, consistent, and simple baseline to build upon. */}
<CssBaseline />
{getLayout(<Component {...pageProps} />)}
</ThemeProvider>
</CacheProvider>
)
}
I took the source from here, and modified it a little bit so could work properly with Next.js due to ssr and client rendering mismatches.
The useLocalStorage also uses another useEventListener hook, to synchronize the changes on the value among all other tabs opened.
useLocalStorage.tsx
// edited from source: https://usehooks-ts.com/react-hook/use-local-storage
// to support ssr in Next.js
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import useEventListener from '@/react/hooks/useEventListener'
type SetValue<T> = Dispatch<SetStateAction<T>>
function useLocalStorage<T>(key: string, initialValue: T): [T, SetValue<T>] {
// Read local storage the parse stored json or return initialValue
const readStorage = (): T => {
if (typeof window === 'undefined') {
return initialValue
}
try {
const item = window.localStorage.getItem(key)
return item ? (parseJSON(item) as T) : initialValue
} catch (error) {
console.warn(`Error reading localStorage key “${key}”:`, error)
return initialValue
}
}
// Persists the new value to localStorage.
const setStorage: SetValue<T> = value => {
if (typeof window == 'undefined') {
console.warn(
`Tried setting localStorage key “${key}” even though environment is not a client`,
)
}
try {
// Allow value to be a function so we have the same API as useState
const newValue = value instanceof Function ? value(state) : value
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(newValue))
// We dispatch a custom event so every useLocalStorage hook are notified
window.dispatchEvent(new Event('local-storage'))
} catch (error) {
console.warn(`Error setting localStorage key “${key}”:`, error)
}
}
// State to store the value
const [state, setState] = useState<T>(initialValue)
// Once the component is mounted, read from localStorage and update state.
useEffect(() => {
setState(readStorage())
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
useEffect(() => {
setStorage(state)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [state])
const handleStorageChange = () => {
setState(readStorage())
}
// this only works for other documents, not the current one
useEventListener('storage', handleStorageChange)
// this is a custom event, triggered in writeValueToLocalStorage
// See: useLocalStorage()
useEventListener('local-storage', handleStorageChange)
return [state, setState]
}
export default useLocalStorage
// A wrapper for "JSON.parse()"" to support "undefined" value
function parseJSON<T>(value: string | null): T | undefined {
try {
return value === 'undefined' ? undefined : JSON.parse(value ?? '')
} catch (error) {
console.log('parsing error on', { value })
return undefined
}
}
The useEventListener.tsx is exactly the same as is in the web.
All of the code of this example can be found in my github repository that can be used as a starting point for a project with Typescript, Nextjs and Material UI.
You can also try a working example here.
Upvotes: 7
Reputation: 702
toggleColorMode: () => {
//use this line for save in localStorage
localStorage.setItem("mode",mode=== 'light' ? 'dark' : 'light' )
setMode((prevMode) =>
prevMode === 'light' ? 'dark' : 'light',
);
},
then write a useEffect to set mode based on localStorage
useEffect(()=>{
if( localStorage.getItem("mode")){
setMode(localStorage.getItem("mode"))
}
},[])
Upvotes: 2