I just can't figure it out. I want to change background with Switch, but it works only the first time, doesn't work on subsequent clicks.
Here is code sandbox:
https://codesandbox.io/s/strange-shaw-2tfk5
Could someone enlighten me what is going on? I'm using Material UI with React
minimal reproducible example:
this is my App component
import React from "react";
import { useState } from "react";
import { ThemeProvider, createTheme } from "#material-ui/core/styles";
import { Container, Switch, CssBaseline } from "#material-ui/core";
const darkTheme = createTheme({
palette: {
type: "dark",
background: {
default: "hsl(230, 17%, 14%)"
}
}
});
const lightTheme = createTheme({
palette: {
type: "light",
background: {
default: "hsl(0, 0%, 100%)"
}
}
});
const App = () => {
const [mode, setMode] = useState("light");
const selectedTheme = mode === "dark" ? darkTheme : lightTheme;
return (
<ThemeProvider theme={selectedTheme}>
<CssBaseline />
<Container maxWidth="lg">
<h1>Hello</h1>
<Switch onChange={() => setMode(mode === "light" ? "dark" : "light")} />
</Container>
</ThemeProvider>
);
};
export default App;
According to the Material UI documentation, to switch between light and dark, they suggest useMemo to create a new theme on demand:
import { useState, useMemo } from "react";
function App() {
const [mode, setMode] = useState("light");
const theme = useMemo(
() =>
createTheme({
palette: {
type: mode,
background: {
dark: "hsl(230, 17%, 14%)",
light: "hsl(0, 0%, 100%)"
}
}
}),
[mode]
);
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<Container maxWidth="lg">
<h1>Hello</h1>
<Switch onChange={() => setMode(mode === "light" ? "dark" : "light")} />
</Container>
</ThemeProvider>
);
}
This works even when using StrictMode.
The reason it doesn't work with StrictMode when defined outside of the component is addressed in a Github issue, which states that this behaviour should be fixed in v5 (still in beta).
not sure why, but removing StrictMode in index.js fixed the problem
You can either put your variables for themes inside your App component:
https://codesandbox.io/s/tender-lederberg-8v7tx?file=/src/App.js
or dynamically change theme properties:
https://codesandbox.io/s/magical-ives-587fm?file=/src/App.js
Related
I'm trying something very simple: building two themes for a website using Material-UI themes:
A light theme and dark one, but it does not work well: the theme is on every Material-UI react element, but the root element on the html document keeps having the same default white background.
Of course it can be changed by attacking the body with pure .css:
body {
background-color: #222;
}
But I was looking to change it dynamically with React, I though this would work, but it does not:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ThemeProvider } from '#material-ui/styles';
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
const themeLight = createMuiTheme({
palette: {
background: {
default: "#e4f0e2"
}
},
});
const themeDark = createMuiTheme({
palette: {
background: {
default: "#222222",
}
},
});
ReactDOM.render(
<MuiThemeProvider theme = { themeDark }>
<App />
</MuiThemeProvider>, document.getElementById('root'));
and I'm lost here, there is no way to make this with Material-UI theme?
CssBaseline is the component that controls this aspect. If you aren't using CssBaseline, then you are just seeing the default provided by the browser.
Here is a working v4 example (v5 example further down):
import React from "react";
import ReactDOM from "react-dom";
import CssBaseline from "#material-ui/core/CssBaseline";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
const themeLight = createMuiTheme({
palette: {
background: {
default: "#e4f0e2"
}
}
});
const themeDark = createMuiTheme({
palette: {
background: {
default: "#222222"
},
text: {
primary: "#ffffff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<MuiThemeProvider theme={light ? themeLight : themeDark}>
<CssBaseline />
<Button onClick={() => setLight(prev => !prev)}>Toggle Theme</Button>
</MuiThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Below is a Material-UI v5 example. The only difference from v4 is the name change for ThemeProvider (though this name is also available in v4 in addition to MuiThemeProvider) and createTheme (instead of createMuiTheme) and using the new #mui/material package name instead of #material-ui/core.
import React from "react";
import ReactDOM from "react-dom";
import CssBaseline from "#mui/material/CssBaseline";
import { ThemeProvider, createTheme } from "#mui/material/styles";
import Button from "#mui/material/Button";
const themeLight = createTheme({
palette: {
background: {
default: "#e4f0e2"
}
}
});
const themeDark = createTheme({
palette: {
background: {
default: "#222222"
},
text: {
primary: "#ffffff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<ThemeProvider theme={light ? themeLight : themeDark}>
<CssBaseline />
<Button onClick={() => setLight((prev) => !prev)}>Toggle Theme</Button>
</ThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In MUI v5, you can also use GlobalStyles component to add the styles to the body element:
<GlobalStyles
styles={{
body: { backgroundColor: "lightyellow" }
}}
/>
On top of #NearHuscarl 's answer, importing the GlobalStyles after the CSSBaseLine, will retain the page defaults (like margin: 0, etc.,) still able to customize root-level / global styles. For eg,
import { Component } from "react";
import { Button, CssBaseline, GlobalStyles } from "#mui/material";
import { ThemeProvider, createTheme } from "#mui/material/styles";
export class App extends Component {
render() {
const theme = createTheme({
palette: {
mode: "dark",
primary: {
main: "#ff0000",
contrastText: "#fff",
},
secondary: {
main: green[500],
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
body: { backgroundColor: "cyan" },
}}
/>
<Button color="primary" variant="contained">
Button
</Button>
</ThemeProvider>
);
}
}
export default App;
(I'm just using class component out of habit 😅)
Full example with nested themes MUI Theme toggle
My use case, I only wanted to change background-color of body from within a React component, not the entire theme. Used a global override.
TL;DR code:
// other imports ...
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
'#global':{
body:{
backgroundColor:"#382E7E"
}
},
otherstyles:{
// other styles ....
},
}));
// React component, etc ...
MUI Version 5
import { createTheme } from "#mui/material/styles";
const themeX = createMuiTheme({
palette: {
mode: "dark",
}
});
MUI Version 4
import { createMuiTheme } from '#material-ui/core/styles';
const themeX = createMuiTheme({
palette: {
type: "dark",
}
});
just as simple that, changing the pallets type to dark by default its set to light. This will also help in custom color for other components like typography, icon etc
ReactDOM doesn't replace the targeted element. I haven't worked with material ui personally. However, if you put the background color you want into your App state as something like 'currentRootColor', then in your App component's render function you could put:
render() {
document.body.style.backgroundColor = this.state.currentRootColor;
...the rest of your App render code
}
This would set the body's background color and if you change 'this.state.currentRootColor', then your App component would re-render with the new background color.
However if you dont already have a < body > tag in your document you would need to add one.
In MUI v5, this seem to work for me. I used it to apply specific styles only to HomePage (overwrite default styles).
pages/HomePage.js
...
import GlobalStyles from '#mui/material/GlobalStyles';
// or
import { GlobalStyles } from '#mui/material';
Note: It is a good practice to hoist the <GlobalStyles /> to a static
constant, to avoid rerendering. This will ensure that the tag
generated would not recalculate on each render.
const homePageStyles = (
<GlobalStyles
styles={{
body: { backgroundColor: 'cyan' },
'.MuiTypography-root': {
color: 'red',
},
}}
/>
);
...
return (
<>
{homePageStyles}
<MyComponents />
</>
);
....
More:
https://mui.com/material-ui/customization/how-to-customize/
https://mui.com/material-ui/api/global-styles/
All the above answers did not work for me, why?? I don't know.
I covered all of my components with ScopedCssBaseline and mui will apply the palette style only to the children.
Below is my answer,
import React from "react";
import ReactDOM from "react-dom";
import { ScopedCssBaseline, Button } from "#mui/material";
import { ThemeProvider, createTheme } from "#mui/material/styles";
const themeLight = createTheme({
palette: {
background: {
default: "#fff"
},
text: {
default: "#000"
}
}
});
const themeDark = createTheme({
palette: {
background: {
default: "#000"
},
text: {
primary: "#fff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<ThemeProvider theme={light ? themeLight : themeDark}>
<ScopedCssBaseline enableColorScheme>
<Button onClick={() => setLight((prev) => !prev)}>Toggle Theme</Button>
</ScopedCssBaseline>
</ThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
I've used Ryan Cogswell's answer to have my project compatible to RTL.
But for some reason, the Material-ui icon <Send/> didn't flip accordingly. Is it because it's not compatible to RTL? Or am I missing something?
Here's an example showing that the Send icon doesn't flip:
import React from "react";
import { create } from "jss";
import rtl from "jss-rtl";
import {
StylesProvider,
jssPreset,
ThemeProvider,
createMuiTheme
} from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
import SendIcon from "#material-ui/icons/Send";
// Configure JSS
const jss = create({ plugins: [...jssPreset().plugins, rtl()] });
const ltrTheme = createMuiTheme({ direction: "ltr" });
const rtlTheme = createMuiTheme({ direction: "rtl" });
function AppContent() {
const [isRtl, setIsRtl] = React.useState(false);
React.useLayoutEffect(() => {
document.body.setAttribute("dir", isRtl ? "rtl" : "ltr");
}, [isRtl]);
return (
<ThemeProvider theme={isRtl ? rtlTheme : ltrTheme}>
<CssBaseline />
<Box m={2}>
<TextField label={isRtl ? "بريد الكتروني او هاتف" : "Email or Phone"} />
<br />
<SendIcon />
<br />
Current Direction: {isRtl ? "rtl" : "ltr"}
<br />
<Button onClick={() => setIsRtl(!isRtl)}>Toggle direction</Button>
</Box>
</ThemeProvider>
);
}
export default function App() {
return (
<StylesProvider jss={jss}>
<AppContent />
</StylesProvider>
);
}
Thanks
Material-UI icons are not automatically flipped for rtl. This is discussed some here: https://github.com/mui-org/material-ui/issues/22726.
Here is an example of one way to handle this for the Send icon (and this approach should be usable for other icons as well):
const DirectionAwareSendIcon = withStyles((theme) => ({
root: {
transform: theme.direction === "rtl" ? "scaleX(-1)" : undefined
}
}))(SendIcon);
It is also possible to handle this globally using overrides in the theme:
MuiSvgIcon: {
root: {
"body[dir=rtl] &": {
transform: "scaleX(-1)"
}
}
}
There is some risk that this could conflict with styling in some Material-UI components which use transform in the default styles, but the examples that I have looked at so far (e.g. AccordionSummary), seem to still work fine. This global approach would currently cause issues for TablePaginationActions and PaginationItem which both swap which icons they use based on theme.direction. This global approach would then flip the already-flipped icon, so if you use either of these components you would need to take this into consideration.
There are also some icons where flipping is not desirable, for instance icons with a recognizable symbol such as Help ("?") and AttachMoney ("$"), so my recommendation would be the first approach of just explicitly adding the flipping behavior to the icons that need it.
Here's a full working example with the theme approach:
import React from "react";
import { create } from "jss";
import rtl from "jss-rtl";
import {
StylesProvider,
jssPreset,
ThemeProvider,
createMuiTheme
} from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
import TextField from "#material-ui/core/TextField";
import Button from "#material-ui/core/Button";
import Box from "#material-ui/core/Box";
import SendIcon from "#material-ui/icons/Send";
const overrides = {
MuiSvgIcon: {
root: {
"body[dir=rtl] &": {
transform: "scaleX(-1)"
}
}
}
};
// Configure JSS
const jss = create({ plugins: [...jssPreset().plugins, rtl()] });
const ltrTheme = createMuiTheme({ direction: "ltr" });
const rtlTheme = createMuiTheme({ direction: "rtl", overrides });
function AppContent() {
const [isRtl, setIsRtl] = React.useState(false);
React.useLayoutEffect(() => {
document.body.setAttribute("dir", isRtl ? "rtl" : "ltr");
}, [isRtl]);
return (
<ThemeProvider theme={isRtl ? rtlTheme : ltrTheme}>
<CssBaseline />
<Box m={2}>
<TextField label={isRtl ? "بريد الكتروني او هاتف" : "Email or Phone"} />
<br />
<SendIcon />
<br />
Current Direction: {isRtl ? "rtl" : "ltr"}
<br />
<Button onClick={() => setIsRtl(!isRtl)}>Toggle direction</Button>
</Box>
</ThemeProvider>
);
}
export default function App() {
return (
<StylesProvider jss={jss}>
<AppContent />
</StylesProvider>
);
}
Currently this is what I am doing, passing a ThemeProvider above my component file:
import React from 'react';
import { ThemeProvider, createMuiTheme } from '#material-ui/core/styles';
import MUIButton from '#material-ui/core/Button';
const theme = createMuiTheme({
palette: {
primary: {
main: "#ff0000"
}
},
typography: {
fontFamily: 'Nunito Sans, sans-serif',
button: {
textTransform: 'none'
}
},
shape: {
borderRadius: 3
}
})
export default ({ variant, children }) => {
return (
<ThemeProvider theme={theme}>
<MUIButton
color="primary"
variant={variant}
>
{children}
</MUIButton>
</ThemeProvider>
)
}
I am trying to figure out how I can do this at a global level in Storybook. This is the first component I have built out called Button. So I want to be able to have the theme in an external file, and have the ThemeProvider coming in at a higher level so I don't have to wrap each component. Hope that makes sense, and if anyone has any ideas.
First, I suggest you to move your theme into a separate file (such as src/stylesheet so you can access it from different files (your global App component and your storybook preview file).
// src/stylesheet.ts
import { createMuiTheme } from '#material-ui/core/styles';
export const muiTheme = createMuiTheme({
palette: {
primary: {
main: "#ff0000"
}
},
typography: {
fontFamily: 'Nunito Sans, sans-serif',
button: {
textTransform: 'none'
}
},
shape: {
borderRadius: 3
}
})
Then, you need to setup your storybook the same way you set up your react app. To do so, try to create a file called:
.storybook/preview.js and put this inside of it:
// .storybook/preview.js
import React from 'react';
import { addDecorator } from '#storybook/react';
import { ThemeProvider } from '#material-ui/core/styles';
import { muiTheme } from '../src/stylesheet';
addDecorator((story) => (
<ThemeProvider theme={muiTheme}>{story()}</ThemeProvider>
));
It will wrap all your stories inside of the ThemeProvider.
In your app, you can can also have a global App component which will encapsulate the whole app within the ThemeProvider
More help:
https://storybook.js.org/docs/basics/writing-stories/
This is the only solution that worked for me with MUI v5:
// .storybook/main.js
const path = require('path');
const toPath = (filePath) => path.join(process.cwd(), filePath);
module.exports = {
webpackFinal: async (config) => {
return {
...config,
resolve: {
...config.resolve,
alias: {
...config.resolve.alias,
'#emotion/core': toPath('node_modules/#emotion/react'),
'emotion-theming': toPath('node_modules/#emotion/react'),
},
},
};
},
};
// .storybook/preview.js
import { ThemeProvider, createTheme } from '#mui/material/styles';
import { ThemeProvider as Emotion10ThemeProvider } from 'emotion-theming';
const defaultTheme = createTheme(); // or your custom theme
const withThemeProvider = (Story, context) => {
return (
<Emotion10ThemeProvider theme={defaultTheme}>
<ThemeProvider theme={defaultTheme}>
<Story {...context} />
</ThemeProvider>
</Emotion10ThemeProvider>
);
};
export const decorators = [withThemeProvider];
// ...other storybook exports
It's from the MUI migration guide but it took me unnecessarily long to find so I want to share it here:
https://mui.com/guides/migration-v4/
For Storybook 6.3+ it should look like the following
Update preview.js with:
import React from 'react';
import { theme } from './src/theme'; // whereever you have defined your material ui theme
export const decorators = [
Story => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
];
More details on how to decorate your stories here:
https://storybook.js.org/docs/react/writing-stories/decorators
And creating your theme for Material UI here:
https://material-ui.com/customization/theming/
This works for me with react-router and mui V5
my preview.js =>
import React from 'react';
import { addDecorator } from '#storybook/react';
import { MemoryRouter } from 'react-router';
import { ThemeProvider } from '#mui/material/styles';
import { ThemeProvider as Emotion10ThemeProvider } from 'emotion-theming';
import customTheme from '../src/theme/index';
addDecorator((story) => (
<Emotion10ThemeProvider theme={customTheme}>
<ThemeProvider theme={customTheme}>
<MemoryRouter initialEntries={['/']}>{story()}</MemoryRouter>
</ThemeProvider>
</Emotion10ThemeProvider>
));
export const parameters = {
actions: { argTypesRegex: '^on[A-Z].*' },
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
};
I believe this is the most up to date answer... and the cleanest, updating path/to/your/Theme with your path
// .storybook/preview.js
import React from 'react';
import { ThemeProvider } from '#mui/system';
import { Theme } from 'path/to/your/Theme';
export const decorators = [
(Story) => (
<ThemeProvider theme={Theme}>
<Story />
</ThemeProvider>
),
];
I'm trying something very simple: building two themes for a website using Material-UI themes:
A light theme and dark one, but it does not work well: the theme is on every Material-UI react element, but the root element on the html document keeps having the same default white background.
Of course it can be changed by attacking the body with pure .css:
body {
background-color: #222;
}
But I was looking to change it dynamically with React, I though this would work, but it does not:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { ThemeProvider } from '#material-ui/styles';
import { MuiThemeProvider, createMuiTheme } from '#material-ui/core/styles';
const themeLight = createMuiTheme({
palette: {
background: {
default: "#e4f0e2"
}
},
});
const themeDark = createMuiTheme({
palette: {
background: {
default: "#222222",
}
},
});
ReactDOM.render(
<MuiThemeProvider theme = { themeDark }>
<App />
</MuiThemeProvider>, document.getElementById('root'));
and I'm lost here, there is no way to make this with Material-UI theme?
CssBaseline is the component that controls this aspect. If you aren't using CssBaseline, then you are just seeing the default provided by the browser.
Here is a working v4 example (v5 example further down):
import React from "react";
import ReactDOM from "react-dom";
import CssBaseline from "#material-ui/core/CssBaseline";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import Button from "#material-ui/core/Button";
const themeLight = createMuiTheme({
palette: {
background: {
default: "#e4f0e2"
}
}
});
const themeDark = createMuiTheme({
palette: {
background: {
default: "#222222"
},
text: {
primary: "#ffffff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<MuiThemeProvider theme={light ? themeLight : themeDark}>
<CssBaseline />
<Button onClick={() => setLight(prev => !prev)}>Toggle Theme</Button>
</MuiThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Below is a Material-UI v5 example. The only difference from v4 is the name change for ThemeProvider (though this name is also available in v4 in addition to MuiThemeProvider) and createTheme (instead of createMuiTheme) and using the new #mui/material package name instead of #material-ui/core.
import React from "react";
import ReactDOM from "react-dom";
import CssBaseline from "#mui/material/CssBaseline";
import { ThemeProvider, createTheme } from "#mui/material/styles";
import Button from "#mui/material/Button";
const themeLight = createTheme({
palette: {
background: {
default: "#e4f0e2"
}
}
});
const themeDark = createTheme({
palette: {
background: {
default: "#222222"
},
text: {
primary: "#ffffff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<ThemeProvider theme={light ? themeLight : themeDark}>
<CssBaseline />
<Button onClick={() => setLight((prev) => !prev)}>Toggle Theme</Button>
</ThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
In MUI v5, you can also use GlobalStyles component to add the styles to the body element:
<GlobalStyles
styles={{
body: { backgroundColor: "lightyellow" }
}}
/>
On top of #NearHuscarl 's answer, importing the GlobalStyles after the CSSBaseLine, will retain the page defaults (like margin: 0, etc.,) still able to customize root-level / global styles. For eg,
import { Component } from "react";
import { Button, CssBaseline, GlobalStyles } from "#mui/material";
import { ThemeProvider, createTheme } from "#mui/material/styles";
export class App extends Component {
render() {
const theme = createTheme({
palette: {
mode: "dark",
primary: {
main: "#ff0000",
contrastText: "#fff",
},
secondary: {
main: green[500],
},
},
});
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<GlobalStyles
styles={{
body: { backgroundColor: "cyan" },
}}
/>
<Button color="primary" variant="contained">
Button
</Button>
</ThemeProvider>
);
}
}
export default App;
(I'm just using class component out of habit 😅)
Full example with nested themes MUI Theme toggle
My use case, I only wanted to change background-color of body from within a React component, not the entire theme. Used a global override.
TL;DR code:
// other imports ...
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles((theme) => ({
'#global':{
body:{
backgroundColor:"#382E7E"
}
},
otherstyles:{
// other styles ....
},
}));
// React component, etc ...
MUI Version 5
import { createTheme } from "#mui/material/styles";
const themeX = createMuiTheme({
palette: {
mode: "dark",
}
});
MUI Version 4
import { createMuiTheme } from '#material-ui/core/styles';
const themeX = createMuiTheme({
palette: {
type: "dark",
}
});
just as simple that, changing the pallets type to dark by default its set to light. This will also help in custom color for other components like typography, icon etc
ReactDOM doesn't replace the targeted element. I haven't worked with material ui personally. However, if you put the background color you want into your App state as something like 'currentRootColor', then in your App component's render function you could put:
render() {
document.body.style.backgroundColor = this.state.currentRootColor;
...the rest of your App render code
}
This would set the body's background color and if you change 'this.state.currentRootColor', then your App component would re-render with the new background color.
However if you dont already have a < body > tag in your document you would need to add one.
In MUI v5, this seem to work for me. I used it to apply specific styles only to HomePage (overwrite default styles).
pages/HomePage.js
...
import GlobalStyles from '#mui/material/GlobalStyles';
// or
import { GlobalStyles } from '#mui/material';
Note: It is a good practice to hoist the <GlobalStyles /> to a static
constant, to avoid rerendering. This will ensure that the tag
generated would not recalculate on each render.
const homePageStyles = (
<GlobalStyles
styles={{
body: { backgroundColor: 'cyan' },
'.MuiTypography-root': {
color: 'red',
},
}}
/>
);
...
return (
<>
{homePageStyles}
<MyComponents />
</>
);
....
More:
https://mui.com/material-ui/customization/how-to-customize/
https://mui.com/material-ui/api/global-styles/
All the above answers did not work for me, why?? I don't know.
I covered all of my components with ScopedCssBaseline and mui will apply the palette style only to the children.
Below is my answer,
import React from "react";
import ReactDOM from "react-dom";
import { ScopedCssBaseline, Button } from "#mui/material";
import { ThemeProvider, createTheme } from "#mui/material/styles";
const themeLight = createTheme({
palette: {
background: {
default: "#fff"
},
text: {
default: "#000"
}
}
});
const themeDark = createTheme({
palette: {
background: {
default: "#000"
},
text: {
primary: "#fff"
}
}
});
const App = () => {
const [light, setLight] = React.useState(true);
return (
<ThemeProvider theme={light ? themeLight : themeDark}>
<ScopedCssBaseline enableColorScheme>
<Button onClick={() => setLight((prev) => !prev)}>Toggle Theme</Button>
</ScopedCssBaseline>
</ThemeProvider>
);
};
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
can you please help me to change the React Material UI theme Dynamically .
https://imgur.com/S8zsRPQ
https://imgur.com/Ul8J40N
I have tried by changing the theme Properties on button click . The theme properties are getting changed as seen in the console . But the change is not reflecting on the theme .
Sandbox Code : https://codesandbox.io/s/30qwyk92kq
const themeChange = () => {
alert(theme);
theme.palette.type = "light";
console.log(theme);
};
ReactDOM.render(
<MuiThemeProvider theme={theme}>
<React.Fragment>
<CssBaseline />
<App changeTheme={themeChange} />
</React.Fragment>
</MuiThemeProvider>,
document.getElementById("app")
);
When I click the button the theme has to change to Dark color
I am using styledComponents, typescript and material-ui.
First I defined my themes:
// This is my dark theme: dark.ts
// I defined a light.ts too
import createMuiTheme from '#material-ui/core/styles/createMuiTheme';
export const darkTheme = createMuiTheme({
palette: {
type: 'dark', // Name of the theme
primary: {
main: '#152B38',
},
secondary: {
main: '#65C5C7',
},
contrastThreshold: 3,
tonalOffset: 0.2,
},
});
I defiend a themeProvider function and in this function I wrapped the material-ui's ThemeProvider in a React context to be able to change the theme easily:
import React, { useState } from 'react';
import {ThemeProvider} from "#material-ui/core/styles/";
import { lightTheme } from "./light";
import { darkTheme } from "./dark";
const getThemeByName = (theme: string) => {
return themeMap[theme];
}
const themeMap: { [key: string]: any } = {
lightTheme,
darkTheme
};
export const ThemeContext = React.createContext(getThemeByName('darkTheme'));
const ThemeProvider1: React.FC = (props) => {
// State to hold the selected theme name
const [themeName, _setThemeName] = useState('darkTheme');
// Retrieve the theme object by theme name
const theme = getThemeByName(themeName);
return (
<ThemeContext.Provider value={_setThemeName}>
<ThemeProvider theme={theme}>{props.children}</ThemeProvider>
</ThemeContext.Provider>
);
}
export default ThemeProvider1;
Now I can use it in my components like this:
import React from 'react';
import styled from 'styled-components';
import useTheme from "#material-ui/core/styles/useTheme";
const MyCardHeader = styled.div`
width: 100%;
height: 40px;
background-color: ${props => props.theme.bgColor};
color: ${props => props.theme.txtColor};
display: flex;
align-items:center;
justify-content: center;
`;
export default function CardHeader(props: { title: React.ReactNode; }) {
const theme = {
bgColor: useTheme().palette.primary.main,
txtColor: useTheme().palette.primary.contrastText
};
return (
<MyCardHeader theme={theme}>
{props.title}
</MyCardHeader>
);
}
For Changing between themes:
import React, {useContext} from 'react';
import { ThemeContext} from './themes/themeProvider';
export default function Header() {
// Get the setter function from context
const setThemeName = useContext(ThemeContext);
return (
<header>
<button onClick={() => setThemeName('lightTheme')}>
light
</button>
<button onClick={() => setThemeName('darkTheme')}>
dark
</button>
</header>
);
}
I'm using Material UI v4.
I tried something like Ashkan's answer, but it didn't work for me.
However, I found this in the documentation, and abstracting it to apply to a different piece of state, instead of user preference, worked for me. For your example, I'd probably make a context:
// context.js
import React, { useContext } from "react";
import { ThemeProvider, createTheme } from "#material-ui/core/styles";
import CssBaseline from "#material-ui/core/CssBaseline";
const CustomThemeContext = React.createContext();
// You can add more to these and move them to a separate file if you want.
const darkTheme = {
palette: {
type: "dark",
}
}
const lightTheme = {
palette: {
type: "light",
}
}
export function CustomThemeProvider({ children }) {
const [dark, setDark] = React.useState(false);
function toggleTheme() {
if (dark === true) {
setDark(false);
} else {
setDark(true);
}
}
const theme = React.useMemo(
() => {
if (dark === true) {
return createTheme(darkTheme);
}
return createTheme(lightTheme);
},
[dark],
);
return (
<CustomThemeContext.Provider value={toggleTheme}>
<ThemeProvider theme={theme}>
<CssBaseline />
{children}
</ThemeProvider>
</CustomThemeContext.Provider>
);
}
export function useToggleTheme() {
const context = useContext(CustomThemeContext);
if (context === undefined) {
throw new Error("useCustomThemeContext must be used within an CustomThemeProvider");
}
return context;
}
Then wrap your app in that:
ReactDOM.render(
<CustomThemeProvider>
<App />
</CustomThemeProvider>,
document.getElementById("app")
);
And then access it in your app:
export default function App(){
const toggleTheme = useToggleTheme();
return (
<div>
<button onClick={toggleTheme}>Toggle the theme!!</button>
</div>
);
}
On a side note, in my app, I actually have a different theme in two sections of the app, based on whether the user is logged in or not. So I'm just doing this:
function App() {
const { authState } = useAuthContext();
const theme = React.useMemo(
() => {
if (authState.user) {
return createTheme(dashboardTheme);
}
return createTheme(loginTheme);
},
[authState.user],
);
return (
<ThemeProvider theme={theme}>
<TheRestOfTheApp />
</ThemeProvider>
}
It seems you can base the theme off any piece of state, or multiple pieces, by referencing them in useMemo and including them in the dependency array.
EDIT:
I just noticed that MUI v5 actually has something very similar in their docs.
In your code, theme type is changed. But the Page is not re-rendered with new theme.
I have changed code in index.js and App.js like following.
Try this approach. It works.
index.js
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
ReactDOM.render(
<App/>,
document.getElementById("app")
);
App.js
import React from "react";
import CssBaseline from "#material-ui/core/CssBaseline";
import Typography from "#material-ui/core/Typography";
import { Button } from "#material-ui/core";
import { MuiThemeProvider, createMuiTheme } from "#material-ui/core/styles";
import blueGrey from "#material-ui/core/colors/blueGrey";
import lightGreen from "#material-ui/core/colors/lightGreen";
class App extends React.Component {
constructor(props){
super(props);
this.state = {
themeType : 'dark',
}
}
changeTheme(){
if (this.state.themeType == 'dark'){
this.setState({themeType:'light'});
} else {
this.setState({themeType:'dark'});
}
}
render() {
let theme = createMuiTheme({
palette: {
primary: {
light: lightGreen[300],
main: lightGreen[500],
dark: lightGreen[700]
},
secondary: {
light: blueGrey[300],
main: blueGrey[500],
dark: blueGrey[700]
},
type: this.state.themeType
}
});
return (
<MuiThemeProvider theme={theme}>
<CssBaseline />
<Typography>Hi there!</Typography>
<Button
variant="contained"
color="secondary"
onClick={()=>{this.changeTheme()}}
>
Change
</Button>
</MuiThemeProvider>
);
}
}
export default App;