Sunday, November 12, 2023

Mastering default styles and props customization in MUI

MUI
UI Library
React

 views

Cover photo for Mastering default styles and props customization in MUI

MUI's theming system is amazing. USE IT

A common phenomenon that I personally observed from beginners is they tend to create wrapper components and apply branding styles and behaviors to it and nothing else, e.g.

my-button.tsx
import { Button, ButtonProps } from '@mui/material';
import { FC } from 'react';
 
export const MyButton: FC<ButtonProps> = (props) => (
  <Button
    sx={{
      py: 2,
      // rest of their branding styles
    }}
    {...props}
  />
);

Will this work? Absolutely, but this method has a drawback. Every member of the team needs to be clear about which wrapper components are created and should be used instead of the original ones from MUI. This can get out of hand when the project and the team size grow. Fortunately, there is actually a more maintainable way to achieve this, which is to make use of MUI's theming system.

Customize the default props

To override the default props of an MUI component, specify those in components.Mui[ComponentName].defaultProps. e.g.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      defaultProps: {
        // accept any props of FilledInput
       disableUnderline: true
      },
    },
  },
});

This will change the default value of disableUnderline for all FilledInput to true, effectively hide its underline by default (and in turn, <TextField variant="filled" />'s underline as well).

One exception here is sx. Though it might be tempting to specify sx here to override the default styling of a component, there is a better place for it, and is much more powerful as well, which we will explore in the next section.

Customize the default styles

Similar to defaultProps, the default styles of an MUI component can be overridden in styleOverrides. e.g.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      styleOverrides: {
        root: {
          backgroundColor: '#fff',
        },
      },
    },
  },
});

This will change the default background color of FilledInput to white (similarly, <TextField variant="filled" />'s background color will also be affected).

One thing to note is that the system prop shorthands, e.g. bgcolor for this case specifically, can NOT be used in styleOverrides. Though Emotion syntax can still be used, e.g.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      styleOverrides: {
        root: {
          backgroundColor: '#fff',
          '&:hover': {
            backgroundColor: '#fbfbfc',
          },
        },
      },
    },
  },
});

Gaining access to the design tokens

One thing you may have noticed from the previous example is that we are hard coding color codes here, which is not the brightest idea since typically, these should be managed by design tokens. These can be addressed by specifying styleOverrides.[slot] as a function instead of an object.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      styleOverrides: {
        root: ({ theme }) => ({
          backgroundColor: theme.palette.common.white
        }),
      },
    },
  },
});

With this, we have access to the theme object we are currently defining in styleOverrides. Thus, can do all sorts of operations that we have gotten used to, like theme.spacing() and theme.breakpoints.up() here.

Conditional rendering

Another use case is to define the default styles depending on the state/props of the component. The function syntax of styleOverrides.[slot] provides ownerState for that purpose. e.g.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      styleOverrides: {
        root: ({ theme, ownerState }) => ({
          // Your CSS
          backgroundColor: ownerState.error
            ? theme.palette.error.light
            : theme.palette.common.white,
        }),
      },
    },
  },
});

Consider the ownerState as a combination of props and internal state. For this example, <FilledInput error />'s background will be red, while <FilledInput />'s will be white.

State classes don't need special handling anymore

If you have been searching online, you may have come across some resources suggesting certain states like error and disabled need special handling, e.g.

theme.ts
export const theme = createTheme({
  components: {
    MuiFilledInput: {
      styleOverrides: {
        root: ({ theme }) => ({
          backgroundColor: theme.palette.common.white,
          '&.Mui-error': {
            backgroundColor: theme.palette.error.light,
          },
        }),
      },
    },
  },
});

This was indeed the case before ownerState was introduced. But nowadays, ownerState can handle them alright, so we don't need to do this anymore.

Written By

Matthew Kwong

Matthew Kwong

Senior Web Engineer @ Viu