Material UI Theming in React
Tags: mui, react, theming
Published: 2023-12-06 17:32:45

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](https://github.com/itsba2/blog_codes/tree/main/mui_theming_react).

## 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:

```shell
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](https://vitejs.dev/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](https://mui.com/material-ui/getting-started/installation/):

```shell
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:

```jsx
// 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](https://mui.com/material-ui/react-css-baseline/) 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.

```jsx
// 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`](https://react.dev/reference/react/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.**

```jsx
// 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`.

```jsx
// 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.

```html
<!-- 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`.

```jsx
// 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](https://mui.com/material-ui/customization/palette/), 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](https://mui.com/material-ui/customization/default-theme/).

```jsx
{
  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](https://mui.com/material-ui/customization/theme-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.

```jsx
{
  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](https://zenoo.github.io/mui-theme-creator/), developed by [@zenoo](https://github.com/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!