We’re Too sx-y for Our Code: Why You Should Ditch MUI’s sx Prop in Your React Components for Theming
I remember the first time I really injured myself. I was probably six years old and my parents were in the backyard, while I was in the garage. I loved playing around the workbench and exploring my dad’s tools. My dad did a great job of keeping the environment safe so I could play with limited supervision. But that afternoon, I reached across the depth of the workbench and found a utility tool that he used when building his remote-controlled airplanes. It was shiny and calling my name: the X-Acto knife! I started digging into his workbench and watched chips of wood come off with a smile on my face.
Then…
Slice!
The edge of the blade cut the pad of my thumb on the way up. I screamed and dropped the blade as I ran to find my parents who were already running into the garage. Fortunately, it didn’t warrant a trip to the hospital and I’m using that thumb to hit the space bar as I type this. But that memory is implanted in my head.
The problem wasn’t the X-Acto knife. It’s a useful tool. Put in the right hands, in the right situation, it works great for its intended use. Used in the wrong manner like, oh, maybe taking chips out of your dad’s workbench, it can be effective and fun, but can also lead to disastrous results. And maybe having your Ninja Turtles taken away for a couple days.
So, What Does This Have to Do With MUI and React?
I’ve written about how we chose React as our framework and how we started migrating our front end to it. One of the tools we decided to use was MUI, which is a React component library built on Material Design (Google’s open-source design system). At ASHTech, we have our own design system, Eve, which is built on top of Material. So, as our engineers started creating components and pages in React, they were matching mockups that were designed to follow Eve, which was (and still is) being developed asynchronously.
Let’s Talk About sx
There are multiple ways to customize or inject styles into an MUI component. This post is only focusing on two: the sx prop and theming. sx
is just a prop that allows you to define styles. It looks similar to the style
attribute in HTML, but works differently. Those styles can use numbers (e.g., 300
), strings with rules similar to CSS (e.g., 1px solid #702F81
), or properties of an MUI theme (e.g., secondary.main
, which is a color property from the MUI theme). It’s very easy to match a mockup. Just find the rules and add them via sx
and voilà!
<Slider
sx={{
width: 300,
border: '1px solid #702F8A',
color: 'secondary.main',
}}
/>
It’s usually the first tool most people reach for when styling an MUI component. It’s like an X-Acto knife: it’s easy to use, simple, versatile, can match any style exactly, and can do more than its intended use.
It can also hurt you.
MUI even states in their documentation that the sx prop is used for a single instance of a component.
The
sx
prop is the best option for adding style overrides to a single instance of a component in most cases. It can be used with all Material UI components.
The Problem With sx
The problem is that at ASHTech, like many organizations, we want our components to have consistency across many — if not all — instances where they are used. After all, that’s one of the main reasons we’re using a component library in the first place! We have over a dozen development teams that create reusable React components for our websites. The team that is building the <Registration />
component also needs to use the smaller building block components like <TextField />
(MUI’s input), <Button />
, etc. that the teams building the <ContactUs />
, <MyAccount />
, <OnlineWorkouts />
, components will also use. We want those to look similar.
You cannot have that consistency if you’re relying heavily on the sx
prop without manually copy/pasting exactly the same props in every instance…and remembering to make any future updates in all those places. I’ve worked in this industry for a long time and with a lot of smart people and I have yet to see somebody who can do this at scale. Nor should they. Engineers should focus on bigger problems rather than remembering the 50 places to copy/paste props to.
Which reminds me what one of those smart front end engineers on our team said recently when it clicked for him why we were trying so hard to stay away from using sx
too often (paraphrasing):
I’ve realized that my job as a front end engineer isn’t to match a mockup exactly. That feels good and is cool to look at, but where I’m most effective is building products for our users that are useful, easy to use, and consistent and spend my time on the bigger issues like building features, improving experience, or performance rather than matching each component to the pixel one at a time.
Bingo! Our users would appreciate a more secure, reliable, faster, and delightful feature over a custom border radius in approximately 100.00% of cases.
sx !== Evil
I want to be clear that I’m not advocating never using sx
. For instance, an organization like ours might want the same <Navigation />
component to have some sx
props since it is a one-off from other components. Other components might need a width
, height
, or even margin
prop.
Put the Knife Down and Back Away
So how do we create our own design system and have flexibility within MUI, while keeping components consistent and allowing for future updates to be in sync when we update our styles?
MUI’s createTheme function and ThemeProvider component allow us to do that. MUI’s documentation states that
Themes let you apply a consistent tone to your app. It allows you to customize all design aspects of your project in order to meet the specific needs of your business or brand.
Here’s a basic example of how a theme works. You can create a theme with as few as one custom property (see palette.primary.main
example below), override as many default MUI theme properties as you want, or even create your own properties.
const theme = createTheme({
palette: {
primary: {
main: purple[500],
},
},
});
<ThemeProvider theme={theme}>
<App />
</ThemeProvider>
Real World Example
Let’s say your mockup has a button with a filled in background and the text “sign up.” If you used MUI’s <Button />
component with contained
variant (to make the UI changes easier to see in this example), you would wind up with this:
The code to get this would be <Button variant="contained">Sign up</Button>
. If you’re not familiar with MUI, a variant
is just a prop you can assign to a component that gives it different UI, sort of like a class. The API documentation for each component lists the available variants and which are default. As with all examples going forward, you can see the sandbox below or by clicking the previous link. All the MUI implimentation in my examples are in Demo.js
.
However, your design system (or mockups if you don’t officially have a design system yet) calls for a purple background with slighly different padding.
You can just pull out the X-Acto knife and carve up a button like this (sandbox):
<Button
variant="contained"
sx={{
padding: ".5rem 1.5rem",
backgroundColor: "#702F8A"
}}
>
Sign up
</Button>
Or you can use a custom theme to change all your buttons. Or at least all buttons of a certain variant (sandbox). We’ll break down the code below.
const theme = createTheme({
palette: {
secondary: {
main: "#702F8A",
light: "#FFEAFF",
dark: "#612A80"
}
},
components: {
MuiButton: {
defaultProps: {
color: "secondary"
},
styleOverrides: {
root: {
padding: ".5rem 1.5rem"
}
}
}
}
});
export default function BasicButtons() {
return (
<ThemeProvider theme={theme}>
<Button
variant="contained"
// sx={
// {
// padding: ".5rem 1.5rem",
// backgroundColor: "#702F8A"
// }
// }
>
Sign up
</Button>
</ThemeProvider>
);
}
Ok, so what’s going on here? First, we’re using createTheme
to create a new theme that will actually extend the MUI default, so you don’t have to redeclare all the standard MUI theme properties. In that theme, we’re assigning new colors for secondary.main
, secondary.light
, and secondary.dark
in the palette
object. Light and dark have nothing to do with light and dark mode — there is support for that as well — but that’s beyond the scope here. This changes the standard secondary color from a different purple to our brand purple, but you can use any color you want. So any MUI components that already have secondary
colors assigned to them by default in MUI will now be purple. We just saved a lot of sx
color props!
Next, we’re modifying the components
object in the theme. In this example, we’re only modifying the button component. We’re using the key of MuiButton
which you can find in any component’s API documentation to add a default prop (defaultProps
) of a secondary
color to all buttons. This is the eqivalent of using <Button color="secondary">
in every usage. Now you just need to use <Button>
, but that can be overwritten in a specific instance by using <Button color="primary">
or another color.
The third change we made was to add custom padding to all buttons via the styleOverrides
key. This is like targeting all button elements in a classic CSS stylesheet with a custom padding property like button {padding: ".5rem 1.5rem"}
. You can see which sx
props are no longer needed with the code that is commented out in the example above.
What About Multiple Variations of a Component
This code works fine for changing all buttons, but what if you only want a specific variant to have styles applied? Let’s suppose your design system wants<Button variant="outlined">
to have a specific background color? In this example, MUI outlined
buttons don’t have a background color, but our design system requires them to have a partially transparent purple color. Remember, we only only want this color on outlined
buttons, not others (sandbox).
Fortunately, MUI gives us access to ownersState
, where we can access public and internal props on the component, such as variant
! The MUI website has great documentation on this that I won’t try to replicate. For this example, we’re able to target outlined
buttons only and change the background color.
import {
+ alpha,
createTheme,
ThemeProvider
} from "@mui/material/styles";
const theme = createTheme({
//...other theme settings...
components: {
MuiButton: {
//...other button customizations...
styleOverrides: {
root: ({ ownerState }) => ({
padding: ".5rem 1.5rem",
+ ...(ownerState.variant === "outlined" && {
+ backgroundColor: alpha(theme.palette.secondary.main, 0.2)
+ })
})
}
}
}
})
Keep in mind that root
targets the root of all buttons. We want consistent padding on all buttons, which is why padding
is in root
. This solution works great for targeting variants that already exist on the component, but one issue we have yet to find an ideal solution for is adding new variants to components and targeting those with the theme.
It’s possible and MUI outlines how to do this, but there are certain components (e.g., <Alert/>
) where adding a new variant loses all of the base colors and styles on the component. In this example, we have to duplicate the default MUI styles which works fine in the first instance, but you have to maintain your styles and sync those with any future MUI updates. We’ve decided it’s a better use of our time to just use the variants MUI gives us in those cases.
How Did We Do This?
In order to do this at scale across a dozen or so teams on a dozen or so websites with different themes that share common components (e.g., our login component, gym search, streaming workouts), it took more than just building and using themes as MUI shows in their docs.
Learning how to use themes was the simple part.
Learning how to toggle themes in the way we wanted to use them was the difficult part.
But for the leadership team, steering over a dozen teams to change the way they write code, while creating designs and building a design system was the really difficult part. In the next part of this series, I’ll explore how we did that.