DEV Community

S. Batuhan Bilmez
S. Batuhan Bilmez

Posted on • Edited on

Material UI Theming in React

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
Enter fullscreen mode Exit fullscreen mode

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 and npm=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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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:

  1. Create a context.
  2. Create a custom hook to use the context from components.
  3. 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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
);
Enter fullscreen mode Exit fullscreen mode

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>
...
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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 of main
  • dark: A darker shade of main
  • contrastText: Text color contrasting main

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,
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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",
        }
      }
    },
  }
}
Enter fullscreen mode Exit fullscreen mode

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)