Material UI Customization (TypeScript)

Material UI Customization (TypeScript)

ยท

8 min read

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>
  • 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>
  • 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>
  • 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>
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",
  },
}));
  • 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>
);
  • 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>
  );
};

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

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,
        }
      }
    }
  }
});

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 />

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>

- 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,
    }
  ),
})}

- 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"));

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>

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

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",
}
  • Using the spread operator
const {backgroundColor, ...myNewProp} = myProp;
  • Deleting the key
import { SystemCssProperties } from "@mui/system";

const myNewProp: SystemCssProperties = myProp;
delete myNewProp.backgroundColor;
  • 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;
  • 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"}]}  />
  • 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",
});
  • Using the merge-sx package You can use the mergeSx() function provided by this package to merge multiple sx properties.
npm install merge-sx
import { mergeSx } from "merge-sx";

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

const mySxProp3: SxProps = mergeSx(mySxProp1, mySxProp2);

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,
  },
});

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)
  ]}
/>

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
    )
  }
/>

๐ŸŽ‰ 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.

ย