In this blog post, we'll be going through the steps to create a stable theme provider for
our React app using Material UI (MUI) components. You can find the corresponding repository for this tutorial here.
Setting Up The Project Basics
Creating React app with Vite
I will cut this part short to more focus on MUI. First of all, we need a running React app. Assuming you already have Node.js installed:
npm create vite@latest my-app -- --template react
cd my-app
npm install
npm run dev
Above command may vary depending on the package manager you use and its version. Please check Vite guide on this. I used
Node.js=v18.13.0
andnpm=v8.19.3
while preparing this document.
Great! You should be able to connect your React app and see a default counter button etc. But we don't want any of that, so we delete every content in App.jsx
and all CSS files: App.css
and index.css
. Remember to delete the lines where they are imported in App.jsx
and main.jsx
.
We're going to start over styling with MUI.
Installing MUI
Now, make sure your app is still breathing (you should be staring at a blank screen, without any build errors from Vite). Then, let's install MUI dependencies. Run the following commands for the default installation as described in the docs:
npm install @mui/material @emotion/react @emotion/styled @mui/icons-material
We should be good to go with a bit of coding now!
Simple login form using MUI
We'll of course need some UI to test our theming. I will create a basic login form (without any functionality) for testing. You can either directly copy and follow along or create your own UI. App.jsx
looks like this:
// App.jsx
import {
Box,
Button,
Card,
CardActions,
CardContent,
TextField,
} from "@mui/material";
function App() {
return (
<Box component="main" sx={{ width: "100%", height: "100vh" }}>
<Card sx={{ maxWidth: "500px", mx: "auto" }}>
<CardContent sx={{ display: "flex", flexDirection: "column", gap: 2 }}>
<TextField label="Email" />
<TextField type="password" label="Password" />
</CardContent>
<CardActions>
<Button sx={{ mx: "auto" }} variant="contained">
Login
</Button>
</CardActions>
</Card>
</Box>
);
}
export default App;
Have you already been disturbed by the scrollbar appeared on your browser? If you inspect the page using DevTools, you'll notice that this occures because we set height: "100vh"
and there are margins around body
element. We have several options to overcome the issue. We can set margin: 0
for body
element via a CSS file or inline CSS. But with MUI, we have a better option. MUI provides a CSS Baseline Component which sets up all basic CSS for you. All you need to do is inserting it into your app like a component. Better wrapping it inside a React.Fragment
component.
// App.jsx
<React.Fragment>
<CssBaseline />
<Box component="main" sx={{ width: "100%", height: "100vh" }}>
<Card sx={{ maxWidth: "500px", mx: "auto" }}>
...
</Card>
</Box>
</React.Fragment>
Voilà! The disturbing scrollbar is gone.
Providing Theme
Creating ThemeProvider with React's useContext
React has an awesome hook called useContext
that lets us read and subscribe to context from a component. The context shall be the theme in our case. I find most of the documents and videos on the internet quite complicated for the context topic. As a result, people end up just copy-pasting the code they find (as I used to do before) and go on. I will try to make sure you understand what we're doing here, not just copy-pasting.
For file structring, I usually put context providers (e.g. Auth or Theme providers) under src/providers/
folder. So, let's create a providers/
directory and a ColorModeProvider.jsx
file under.
We'll follow 3 steps to write our ColorModeProvide
:
- Create a context.
- Create a custom hook to use the context from components.
- Create a provider to distribute the context across the app.
Our ThemeProvider
, or ColorModeProvider
as we named it, should look like below. Please follow along the inline comments.
// ColorModeProvider.jsx
import { createContext, useContext, useState, useMemo } from "react";
import { ThemeProvider, createTheme } from "@mui/material";
// Create context
const ColorModeContext = createContext();
// Create a hook to use ColorModeContext in our components
export const useColorMode = () => {
return useContext(ColorModeContext);
};
// Create ColorModeProvider to wrap our app inside
// and distribute ColorModeContext
export const ColorModeProvider = ({ children }) => {
// We'll be storing color-mode value in the local storage
// So let's fetch that value
const [colorMode, toggleColorMode] = useState(
localStorage.getItem("color-mode")
);
// Context value object to be provided
const value = useMemo(
() => ({
// toggleColorMode method toggles `color-mode` value
// in local storage and colorMode state between `dark` and `light`
toggleColorMode: () => {
if (localStorage.getItem("color-mode") === "light") {
localStorage.setItem("color-mode", "dark");
} else {
localStorage.setItem("color-mode", "light");
}
toggleColorMode((prev) => (prev === "light" ? "dark" : "light"));
},
colorMode,
}),
// Make sure colorMode is in the dependency array
// Otherwise, colorMode context value won't be updating
// although colorMode state value changes.
// We see this behavior because useMemo hook caches
// values until the values in the dependency array changes
[colorMode]
);
// Theme object to be provided
const theme = useMemo(
() =>
createTheme({
palette: {
mode: colorMode, // Set mode property
...(colorMode === "dark"
? // If colorMode is `dark`
{
primary: {
main: "#f06292",
},
}
: // If colorMode is `light`
{
primary: {
main: "#1e88e5",
},
}),
},
}),
// Remember to add colorMode to dependency array
// Otherwise, palette.mode property wont be updating
// resulting in unchanged theme
[colorMode]
);
// Return provider
return (
// We wrap our own context provider around MUI's ThemeProvider
<ColorModeContext.Provider value={value}>
<ThemeProvider theme={theme}>{children}</ThemeProvider>
</ColorModeContext.Provider>
);
};
Wrapping app around ColorModeProvider
We also need to wrap our app with the ColorModeProvider
. Simply go to main.jsx
file and put App
component inside ColorModeProvider
.
// main.jsx
ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode>
<ColorModeProvider>
<App />
</ColorModeProvider>
</React.StrictMode>
);
Modifying index.html
for color mode
Since user doesn't have a color-mode
key-value pair in the local storage initially, we want to ensure the color mode value is there. If not, ThemeProvider
will provide the color palette for the light theme (see the code block in previous section). So we add the following JS lines between a script
tag inside body
element. This code block searches for "color-mode" key-value pair in the local storage. If there isn't a color mode set previously, it sets system theme initially.
<!-- index.html -->
...
<script>
// If there is no color mode set previously
// set system theme initially
if (!localStorage.getItem("color-mode")) {
const isSystemDark = window.matchMedia("(prefers-color-scheme: dark)");
if (isSystemDark.matches) {
localStorage.setItem("color-mode", "dark");
} else {
localStorage.setItem("color-mode", "light");
}
}
</script>
...
Color mode toggler
We also want to create a button to toggle between dark and light. Let's create a dynamic icon button of which icon changes according to theme. Under src/
folder, we create ThemeToggler.jsx
file. This component should simply toggle the color mode on click by using custom useColorMode
hook we created to distribute via ColorModeProvider
.
// ThemeToggler.jsx
import { IconButton } from "@mui/material";
import { DarkMode, LightMode } from "@mui/icons-material";
import { useColorMode } from "./providers/ColorModeProvider";
export const ThemeToggler = () => {
// From our custom hook we get
// toggler function and current color mode
const { toggleColorMode, colorMode } = useColorMode();
return (
<IconButton onClick={toggleColorMode}>
{colorMode === "dark" ? <LightMode /> : <DarkMode />}
</IconButton>
);
};
By now, you should have a working theme toggler button and be able to see how the theme changes between light and dark.
Dealing with Theme Object
Color palette
If we'd like to change the coloring, we modify palette
property of the theme object in ColorModeProvider
. As shown previously, we'll be providing key-value pairs into this object depending on light/dark mode.
As mentioned in the official docs, four tokens represent palette colors:
-
main
: The main shade -
light
: A lighter shade ofmain
-
dark
: A darker shade ofmain
-
contrastText
: Text color contrastingmain
Below are the theme object properties that I occasionally play with. For further details, see MUI default theme.
{
palette: {
mode: Enum["light", "dark"],
primary: {
main: ColorCode,
light: ColorCode,
dark: ColorCode,
contrastText: ColorCode,
},
secondary: {
...
},
error: {
...
},
warning: {
...
},
info: {
...
},
success: {
...
},
text: {
primary: ColorCode,
secondary: ColorCode,
disabled: ColorCode,
},
background: {
default: ColorCode,
paper: ColorCode,
}
}
}
I will not do much coloring for my simple UI but it's important you understand how you change accent, text or background colors.
Changing component styles
Although you can always choose to use default components styles, MUI gives you the freedom to override the initial styles by modifying components
property. For your further reading, themed components are very well explained in the official docs.
As a show case, let's modify MUI's button a bit. We're going to override the root
styles of the component MuiButton
. For instance, let's make button text normal (instead of the default uppercase) and buttons fully rounded.
{
palette: {...},
components: {
MuiButton: {
styleOverrides: {
root: {
// remove uppercase
textTransform: "none",
// make full rounded
borderRadius: "9999px",
}
}
},
}
}
These changes will apply to all components using
MuiButton
component under the hood.
Bonus: MUI Theme Creator
One of the best parts of using MUI is it has a great and active community. MUI Theme Creator, developed by @zenoo, is a useful tool for creating your own MUI theme object. Plus, you don't need to worry about which theme object property to edit. You can also find some extra features (snippets) for your theming. Give it a try!
Last Words
I really find using MUI easy and fun. I hope you have found this blog post easy to understand and fun to follow. If you have read until this point, get yourself a cup of coffee, which you well deserved. Thank you and see you later!
Top comments (0)