MUI (or Material UI) is a popular React library for building feature-rich UI. There are many great competitors like Ant Design, Shadcn, and so on. However, MUI is still my preferred choice when I need to create a robust enterprise frontend application. The library has a rich set of components, flexible theming capabilities, and strong accessibility out of the box.
Because MUI is powerful, things can often get messy. You’ve probably experienced this frustration firsthand:
I’ve been using the library since version 4 on many of my projects. In this article, I share 3 quick tips I learned the hard way. So you can save hours of refactoring and develop more maintainable and testable components:
Okay, let’s get started cleaning up the mess. Here’s the first rule.
Rule number uno - theme it or leave it
Always start exploring customization possibilities from global theme config.
The global theme config centralizes styles for all MUI components, ensuring consistent styling throughout the entire app. That’s why, when you make some adjustment to the Material UI component inside the global theme, it will be applied to all the instances of this component across the app.
Let’s take a button as an example. MUI’s default button is in Material Design 2 style:
But what if we want it to match GitHub’s Primer looks:
For that, we create a new theme config and customize the MuiButton
component through it:
// theme.tsx
import { createTheme } from "@mui/material";
export const theme = createTheme({
components: {
// Name of the component
MuiButton: {
styleOverrides: {
root: {
textTransform: "none",
boxShadow: "none",
padding: "0 12px",
height: 32,
borderRadius: 6,
backgroundColor: "rgb(45, 164, 78)",
border: "1px solid rgba(27, 31, 36, 0.15)",
},
},
},
},
});
NOTE: Always use colors from the palette. Don’t put them directly as strings into your theme config. I did to avoid a complex setup and focus strictly on the rule.
Don’t forget to use ThemeProvider
to inject a custom theme into your application:
// main.tsx
//...
createRoot(document.getElementById("root")!).render(
<StrictMode>
<CssBaseline />
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
</StrictMode>
);
But what if the theme doesn’t give you enough flexibility? That’s where our second rule comes in.
Rule number two - styled component is what you do
Did you carefully investigate all the design tokens and props the theme provides and couldn’t find the desired one for the component you want to customize? It’s time to take the next step - create a styled component.
Let’s proceed with the button. Assume we want to have 2 shapes: round
and square
. By default, MUI doesn’t provide us with the shape
property, but we can easily add it by creating a new styled component:
// button.styled.ts
import {
Button as MuiButton,
styled,
type ButtonProps as MuiButtonProps,
} from "@mui/material";
// Determines whether a given prop should be forwarded to the underlying DOM element.
const shouldForwardProp = (prop: string) => {
if (!prop || typeof prop !== "string") return false;
return !prop.trim().startsWith("$");
};
type ButtonProps = MuiButtonProps & {
$shape?: "round" | "square";
};
export const Button = styled(MuiButton, { shouldForwardProp })<ButtonProps>(
({ $shape = "square" }) => {
const shapeStyle = {
round: {
borderRadius: 100,
},
square: {},
};
return shapeStyle[$shape];
}
);
Important takeaways from the code above:
- Always add dollar
$
or another valid sign to the beginning of your custom property. Then use shouldForwardProp utility function to prevent the custom prop from being forwarded to the underlying DOM element. This stops MUI from throwing “Invalid attribute name:shape
” in the console. Read why unknown DOM props are bad. - Use
styled
function from@mui/material
package, not from@emotion/styled
. Emotion has a different syntax and is not aware of all MUI components and internal structures, which Material UI adds on top of Emotion. That’s why you may run into errors or unsupported cases if you try to customize the MUI component directly with Emotion.
Now, make sure you import your styled Button component instead of MUI’s default one everywhere you use it:
// App.tsx
import { Stack } from "@mui/material";
// Import custom styled component, not MUI's one
**import { Button } from "./button.styled";**
function App() {
return (
<Stack p={2} spacing={1} sx={{ width: 100 }}>
<Button variant="contained">Square</Button>
<Button variant="contained" $shape="round">
Round
</Button>
</Stack>
);
}
export default App;
The result:
If the theme doesn’t cover your needs, you may also be tempted to use the sx
prop. Here’s why creating a styled component works better.
Why not to use sx
?
The SX prop is fine for quickly adding 2-3 theme-aware inline styles. But if you have more complex styling logic, consider using styled components, like I showed above. With this approach, you keep your styling and business logic separate, which improves readability and maintainability.
Sometimes customization goes beyond styling. In that case, it’s time for our third rule.
Style can’t solve? React component's the resolve
Pretend we have a requirement to add a sound effect every time a user clicks on the button. Styled components cannot achieve this, and MUI doesn’t provide any design tokens or capabilities for adding sound either.
When customization involves complex logic, we create a new React wrapper around the styled button:
// Button.tsx
import useSound from "use-sound";
import buttonSound from "../public/button.mp3";
import type { FC } from "react";
import { type ButtonProps, Button as ButtonStyled } from "./button.styled";
export const Button: FC<ButtonProps> = (props) => {
const [play] = useSound(buttonSound);
return <ButtonStyled {...props} onClick={() => play()}></ButtonStyled>;
};
NOTE: To play sound, I use useSound hook. Find sound effects on freesound.org.
Now use the new button component inside your app:
// App.tsx
import { Stack } from "@mui/material";
// Import custom wrapper component, not MUI's or styled one
**import { Button } from "./Button";**
function App() {
return (
<Stack p={2} spacing={1} sx={{ width: 100 }}>
<Button variant="contained">Square</Button>
<Button variant="contained" $shape="round">
Round
</Button>
</Stack>
);
}
export default App;
Now that you’ve seen all three rules in action, let’s wrap it up.
Conclusion
Congratulations! 🎉
Now you know how to keep your MUI-based design system tidy. You’ve learned:
- Why and how to customize components through the global theme wherever it makes sense.
- In what cases use styled components.
- In what cases you should create a separate React wrapper over the MUI component.
Without this hierarchy, you’ll end up mixing theme overrides, inline sx
, and wrapper hacks in random places.
By applying these three easy-to-follow rules, you’re going to build design systems that are easy to maintain, test, and document. Your colleagues and future self will very likely appreciate it 🙂
Want more? 🔥
Check my previous article “React MVVM Architecture with TanStack Router: An Introduction” where I teach good old React MVVM with a modern tech stack.
Creating content like this is not an easy ride, so I hope you’ll hit that like button and drop a comment. As usual, constructive feedback is always welcome!
📰 Read me on the platform of your choice: Substack, DEV.to
Till next time! 👋
Top comments (0)