DEV Community

Cover image for Material UI Customization (TypeScript)
Rashid Shamloo
Rashid Shamloo

Posted on

Material UI Customization (TypeScript)

This is a summary of different customizations I did while building one of my projects. you can read more about it here.

- Component Customization

I've used multiple methods of customizing Material UI components in this project:

  • Using inline properties and style:
import { Typography } from "@mui/material";

<Typography
  fontSize={18}
  fontWeight={600}
  style={{ textDecoration: "line-through" }}
>
  TEXT
</Typography>
Enter fullscreen mode Exit fullscreen mode
  • Using the sx property which provides access to the theme and breakpoints and some shorthand properties like p and m instead of padding and margin:
import { Typography, SxProps, Theme } from "@mui/material";

const MyStyles: SxProps<Theme> = (theme: Theme) => ({
  mt: 7,
  fontSize: {
    xs: theme.typography.h4.fontSize,
    md: theme.typography.h3.fontSize,
  },
  fontWeight: 600,
});

<Typography sx={MyStyles}>TEXT</Typography>
Enter fullscreen mode Exit fullscreen mode
  • Setting the style on the parent by directly targeting the child's Mui class:

(in this example "&>p" would work too and this method is more suited for other components like Switch and classes like ".MuiSwitch-thumb")

import { Box, Typography } from "@mui/material";

<Box
  sx={{ "&>.MuiTypography-root": { fontSize: 18, fontWeight: 600 } }}
>
  <Typography>Text</Typography>
</Box>
Enter fullscreen mode Exit fullscreen mode
  • Setting the style on the parent and using inherit in the child

You can set component properties to have the value of "inherit", in which case they inherit the style of their parent element.

import { Box, Typography } from "@mui/material";

<Box
  sx={{ fontSize: 18, fontWeight: 600 }}
>
  <Typography fontSize="inherit" fontWeight="inherit">
    Text
  </Typography>
</Box>
Enter fullscreen mode Exit fullscreen mode
import { Typography, TypographyProps, styled() } from "@mui/material";

const CustomTypography = styled(Typography)<TypographyProps>(({ theme }) => ({
  fontSize: 18,
  fontWeight: 600,
  [theme.breakpoints.up("xs")]: {
    textAlign: "center",
  },
  [theme.breakpoints.up("md")]: {
    textAlign: "left",
  },
}));
Enter fullscreen mode Exit fullscreen mode
  • Using a wrapper component:
import { Typography, TypographyProps } from "@mui/material";

const CustomTypography = (props: TypographyProps) => (
  <Typography
    fontSize={18}
    fontWeight="600"
    sx={{ textAlign: { xs: "center", md: "left" } }}
    {...props}
  >
    {props.children}
  </Typography>
);
Enter fullscreen mode Exit fullscreen mode
  • Using the combination of both the styled() utility and a wrapper component:
import { Link, LinkProps, styled() } from "@mui/material";

const CustomLink = (props: LinkProps) => {
  const MyLink = styled(Link)<LinkProps>(({ theme }) => ({
    color: "inherit",
    transition: theme.transitions.create(["color"], {
      duration: theme.transitions.duration.standard,
    }),
    "&:hover": {
      color: theme.palette.strongCyan.main,
    },
  }));
  return (
    <MyLink {...props} underline="none" rel="noopener">
      {props.children}
    </MyLink>
  );
};
Enter fullscreen mode Exit fullscreen mode

- Theming

You can customize the Material UI theme by changing/adding custom colors to the palette or setting a custom font to be used by default. then by wrapping your component in a <ThemeProvider>, the theme will be available to the child components:

import {
  ThemeProvider,
  createTheme,
  PaletteColor,
  SimplePaletteColorOptions,
} from "@mui/material/styles";

declare module "@mui/material/styles" {
  interface Palette {
    strongCyan: PaletteColor;
  }

  interface PaletteOptions {
    strongCyan: SimplePaletteColorOptions;
  }
}

const theme = createTheme({
  palette: {
    strongCyan: { main: "hsl(171, 66%, 44%)" },
  },
  typography: {
    fontFamily: "'Bai Jamjuree', 'sans-serif';",
  },
});

...

<ThemeProvider theme={theme}>
  <ChildComponent />
</ThemeProvider>
Enter fullscreen mode Exit fullscreen mode

You can also customize your components globally using the theme:

const theme = createTheme({
  components: {
    // component
    MuiLink: {
      // change property defaults
      defaultProps: {
        underline: "hover"
      },
      // override CSS
      styleOverrides: {
        // Mui class
        root: {
           fontWeight: 600,
        }
      }
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

You can define new variants for your components in the theme and use nested themes as well.

- CSS Reset / Normalize

Some elements have margin and padding values applied by default which can mess up the layout. Material UI provides a handy component called <CssBaseline> that acts as a CSS Reset and removes those nasty default stylings:

import { CssBaseline } from "@mui/material";
...
<CssBaseline />
<YourOtherComponents />
Enter fullscreen mode Exit fullscreen mode

In order to apply <CssBaseline> only to some of your components, you can use the <ScopedCssBaseline> component instead:

import { ScopedCssBaseline } from "@mui/material";
...
<Component />
<ScopedCssBaseline>
  <AffectedComponent />
</ScopedCssBaseline>
Enter fullscreen mode Exit fullscreen mode

- Transitions

To add transitions to Material UI components, you can use the theme.transitions.create() function which takes the properties you want to apply transition to as the first argument and a settings object as the second. you can set the duration to the value defined in the theme so it's easy to adjust/change at a later stage:

sx={(theme) => ({
  transition: theme.transitions.create(
    ["color", "background-color"],
    {
      duration: theme.transitions.duration.standard,
    }
  ),
})}
Enter fullscreen mode Exit fullscreen mode

- Media Queries

Material UI provides a handy useMediaQuery() hook we can use to detect the screen size and do things like showing/hiding a component on certain screen sizes or in my case, disabling animation delays on smaller screens.
You can use it like this:

import { useMediaQuery, Theme } from "@mui/material";
...
const matches = useMediaQuery((theme: Theme) => theme.breakpoints.up("md"));
Enter fullscreen mode Exit fullscreen mode

In this case, matches will be true if the screen is bigger than md (medium) and false if it's not. then you can use it like any other boolean variable to add conditions to your logic/render.
You can also use exact pixel values: useMediaQuery('(min-width: 900px)')

- Customizing the child component of another component

Some components in Material UI have other nested components inside of them. for example a Dialog component has a Paper component inside. and in order to customize the properties of the nested component, it exposes a property called PaperProps which you can use to do that. You will have to check the Material UI API to know all the properties available for each component.

<Dialog
  PaperProps={{
    sx: {
      borderRadius: '1rem'
    },
  }}
>
...
</Dialog>
Enter fullscreen mode Exit fullscreen mode

- Forwarding ref to component children

Some components like Tooltip need to assign ref to their children to work properly, which means if you place a custom component inside a Tooltip component, you then have to use React.forwardRef() with your custom component so it accepts a ref. this is how I implemented a custom Link inside a custom Tooltip component:

import React from "react";
import { Link, Tooltip, LinkProps, TooltipProps } from "@mui/material";

// custom Tooltip wrapper component
const MyTooltip = (props: TooltipProps) => (
  <Tooltip
    {...props}
    arrow
    placement="top"
    TransitionComponent={Fade}
    TransitionProps={{ timeout: 500 }}
  >
    {props.children}
  </Tooltip>
);

// custom Link wrapper component
const MyLink = React.forwardRef<HTMLAnchorElement, LinkProps>(
  (props, ref) => {
    const linkStyles = (theme: Theme) => ({
      transition: theme.transitions.create(["filter", "transform", "border"], {
        duration: theme.transitions.duration.standard,
      }),
      "&:hover": {
        filter: "brightness(150%)",
        transform: "scale(1.2)",
      },
    });
    return (
      <Link {...props} target="_blank" rel="noopener" sx={linkStyles} ref={ref}>
        {props.children}
      </Link>
    );
  }
);

...

<MyTooltip title="React.js">
  <MyLink href="https://react.dev" target="_blank">
    React.js
  </MyLink>
</MyTooltip>
Enter fullscreen mode Exit fullscreen mode

Instead of
React.forwardRef<HTMLAnchorElement, LinkProps>(props, ref)
you can use
React.forwardRef((props: LinkProps, ref: React.Ref<HTMLAnchorElement>)

- Modifying / Merging sx properties

Sometimes you need to use an sx property you've already defined but change or remove some properties from it. There are multiple ways to do this:

1. Removing properties from the sx prop

Imagine we want to remove backgroundColor from the sx prop below:

import { SxProps } from "@mui/material";

const myProp: SxProps = {
  color: "red",
  backgroundColor: "blue",
}
Enter fullscreen mode Exit fullscreen mode
  • Using the spread operator
const {backgroundColor, ...myNewProp} = myProp;
Enter fullscreen mode Exit fullscreen mode
  • Deleting the key
import { SystemCssProperties } from "@mui/system";

const myNewProp: SystemCssProperties = myProp;
delete myNewProp.backgroundColor;
Enter fullscreen mode Exit fullscreen mode
  • Resetting the key by merging

You can reset the backgroundColor property by merging your sx prop with another sx prop like this: {backgroundColor: "transparent"}. merging is explained in the next sections.

2. Adding / Modifying properties of sx prop

  • Adding/Modifying the key
myProp.backgroundColor = "green";
myProp.mt = 2;
Enter fullscreen mode Exit fullscreen mode
  • You can also accomplish this by merging your style with another which will add/replace the required keys. (explained next)

3. Merging multiple sx properties

  • Using sx Array

sx prop accepts an array as input which can contain multiple sx properties that will be merged together:

<ComponentName sx={[{color: "red"},{backgroundColor: "blue"}]}  />
Enter fullscreen mode Exit fullscreen mode
  • Using Object.assign()

sx properties are objects, so you can use Object.assign() to merge multiple sx properties together:

const myNewProp: SxProps = Object.assign(myProp, {
  backgroundColor: "green",
});
Enter fullscreen mode Exit fullscreen mode
  • Using the merge-sx package You can use the mergeSx() function provided by this package to merge multiple sx properties.
npm install merge-sx
Enter fullscreen mode Exit fullscreen mode
import { mergeSx } from "merge-sx";

const mySxProp1: SxProps = { ... }
const mySxProp2: SxProps = { ... }

const mySxProp3: SxProps = mergeSx(mySxProp1, mySxProp2);
Enter fullscreen mode Exit fullscreen mode

The good thing about this package is that it works with functional SxProps as well, which I'll explain next.

4. Dealing with functional sx properties

sx properties can also be functions that take the theme as input and return an SxProps object:

import { SxProps, Theme } from "@mui/material";

const myStyles: SxProps<Theme> = (theme: Theme) => ({
  fontSize: {
    xs: theme.typography.h4.fontSize,
    md: theme.typography.h3.fontSize,
  },
});
Enter fullscreen mode Exit fullscreen mode

This way you can use the variables in your theme inside your sx prop.
But what this means is that you can't use Object.assign() or modify the keys directly because you're not dealing with objects anymore.
In this case, the best way is to use the sx array method. just be sure to pass the theme to sx functional properties too. and also you will need to use a more specific type for your sx prop:

import { SxProps, Theme } from "@mui/material";

// wrong type ("This expression is not callable." error)
const myStyle1 : SxProps<Theme> = (theme: Theme) => ({ ...

// correct type
import { SystemStyleObject } from "@mui/system";
type SxPropsFunc<T extends object> = (_: T) => SystemStyleObject<T>;
const myStyle1 : SxPropsFunc<Theme> = (theme: Theme) => ({...

// wrong ("No overload matches this call." error)
<Component sx={[myStyle1, myStyle2]} />

// correct
<Component
  sx={[
    (theme:Theme) => myStyle1(theme), 
    (theme:Theme) => myStyle2(theme)
  ]}
/>
Enter fullscreen mode Exit fullscreen mode

Figuring out the correct type above took a good chunk of time...

Overall when merging, you can use the merge-sx package mentioned before to save yourself some trouble. you can also pass the theme:

import { SxProps, Theme } from "@mui/material";

const myStyles: SxProps<Theme> = (theme: Theme) => {...

<MyComponent
  sx={
    mergeSx<Theme>(
      (theme:Theme) => {
        fontSize: theme.typography.h4.fontSize
      },
      myStyles
    )
  }
/>
Enter fullscreen mode Exit fullscreen mode

🎉 That wraps up the Material UI customizations I did for this project. you can read about the rest of the process in this blog post.

Top comments (4)

Collapse
 
sergeyleschev profile image
Sergey Leschev

Great article! We also frequently use Material UI in our TypeScript and React projects and these customizations are definitely helpful. Thanks for sharing your different methods of customizing the components, especially the use of the styled() utility and wrapper components.

Collapse
 
rashidshamloo profile image
Rashid Shamloo

Thank you. I'm glad you've found it to be helpful.

Collapse
 
brense profile image
Rense Bakker

You can also override defaultProps and styles of each component when creating a theme! mui.com/material-ui/customization/...

Collapse
 
rashidshamloo profile image
Rashid Shamloo

Thanks for pointing it out. I've added the info to the "Theming" section.