This guide documents the migration from Material-UI v4 (@material-ui/core) to MUI v5 (@mui/material) with a focus on replacing makeStyles with the styled API.
- Better React 18 compatibility: MUI v5 fully supports React 18's concurrent features
- Improved performance: Emotion-based styling is more performant than JSS
- Smaller bundle size: No JSS runtime overhead
- Better TypeScript support: Improved type inference
- Active maintenance: MUI v5 receives active updates and bug fixes
| MUI v4 | MUI v5 |
|---|---|
@material-ui/core |
@mui/material |
@material-ui/icons |
@mui/icons-material |
@material-ui/lab |
@mui/lab |
@material-ui/styles |
@mui/material/styles |
makeStyles |
styled or sx prop |
createMuiTheme |
createTheme |
yarn add @mui/material @mui/icons-material @mui/lab @emotion/react @emotion/styledyarn remove @material-ui/core @material-ui/icons @material-ui/lab @material-ui/stylesA codemod script is available to help automate the migration from makeStyles to styled:
# Dry run (preview changes without modifying files)
node scripts/migrate-makestyles-to-styled.cjs src/app/core/components/Avatar.tsx --dry-run
# Migrate a single file
node scripts/migrate-makestyles-to-styled.cjs src/app/core/components/Avatar.tsx
# Migrate a directory
node scripts/migrate-makestyles-to-styled.cjs src/app/plugins/openstack/components/
# Verbose output
node scripts/migrate-makestyles-to-styled.cjs src/app/core/ --verbose- Finds files using
makeStylesfrom@mui/stylesor@material-ui/styles - Converts
makeStylesdefinitions tostyledcomponents - Removes
useStyles()calls - Updates imports accordingly
- Adds
TODOcomments where you need to replaceclassNamewith the new styled component
- Replace
<div className={classes.xxx}>with<StyledComponentName> - Verify theme type annotations are correct
- Review any dynamic styles or props
- Test the component thoroughly
Before (MUI v4):
import { makeStyles } from '@material-ui/styles'
import Theme from 'core/themes/model'
const useStyles = makeStyles<Theme>((theme) => ({
container: {
display: 'grid',
gap: 16,
padding: theme.spacing(2),
},
header: {
backgroundColor: theme.components.header.background,
},
}))
export default function MyComponent() {
const classes = useStyles()
return (
<div className={classes.container}>
<div className={classes.header}>Header</div>
</div>
)
}After (MUI v5):
import { styled } from '@mui/material/styles'
import Theme from 'core/themes/model'
const Container = styled('div')(({ theme }: { theme: Theme }) => ({
display: 'grid',
gap: 16,
padding: theme.spacing(2),
}))
const Header = styled('div')(({ theme }: { theme: Theme }) => ({
backgroundColor: theme.components.header.background,
}))
export default function MyComponent() {
return (
<Container>
<Header>Header</Header>
</Container>
)
}Before:
const useStyles = makeStyles({
wrapper: {
display: 'flex',
alignItems: 'center',
gap: 8,
},
})After:
const Wrapper = styled('div')({
display: 'flex',
alignItems: 'center',
gap: 8,
})Before:
const useStyles = makeStyles<Theme, { isActive: boolean }>((theme) => ({
button: {
backgroundColor: ({ isActive }) =>
isActive ? theme.palette.primary.main : theme.palette.grey[300],
},
}))
function MyButton({ isActive }) {
const classes = useStyles({ isActive })
return <button className={classes.button}>Click</button>
}After:
interface StyledButtonProps {
isActive: boolean
}
const StyledButton = styled('button')<StyledButtonProps>(({ theme, isActive }) => ({
backgroundColor: isActive
? (theme as Theme).palette.primary.main
: (theme as Theme).palette.grey[300],
}))
function MyButton({ isActive }: { isActive: boolean }) {
return <StyledButton isActive={isActive}>Click</StyledButton>
}The sx prop is great for quick, inline styles that need theme access:
import Box from '@mui/material/Box'
function MyComponent() {
return (
<Box
sx={{
display: 'grid',
gap: 2,
p: 2,
backgroundColor: (theme) => theme.components.frame.background,
}}
>
Content
</Box>
)
}Before:
const useStyles = makeStyles((theme) => ({
customButton: {
borderRadius: 8,
textTransform: 'none',
},
}))
function MyComponent() {
const classes = useStyles()
return <Button className={classes.customButton}>Click</Button>
}After:
import Button from '@mui/material/Button'
import { styled } from '@mui/material/styles'
const CustomButton = styled(Button)({
borderRadius: 8,
textTransform: 'none',
})
function MyComponent() {
return <CustomButton>Click</CustomButton>
}Before:
import { createTheme as createMuiTheme } from '@material-ui/core/styles'
import { ThemeProvider } from '@material-ui/styles'After:
import { createTheme, ThemeProvider } from '@mui/material/styles'The custom theme interface needs to extend the MUI v5 theme:
// core/themes/model.ts
import { Theme as MuiTheme } from '@mui/material/styles'
// ... existing interfaces ...
type Theme = MuiTheme & AppTheme
export default Theme| v4 Import | v5 Import |
|---|---|
import { makeStyles } from '@material-ui/styles' |
import { styled } from '@mui/material/styles' |
import { Theme } from '@material-ui/core' |
import { Theme } from '@mui/material/styles' |
import Button from '@material-ui/core/Button' |
import Button from '@mui/material/Button' |
import { createMuiTheme } from '@material-ui/core/styles' |
import { createTheme } from '@mui/material/styles' |
import { ThemeProvider } from '@material-ui/styles' |
import { ThemeProvider } from '@mui/material/styles' |
Follow these conventions for consistency:
- Container elements:
[Purpose]Container(e.g.,PageContainer,CardContainer) - Wrapper elements:
[Purpose]Wrapper(e.g.,ButtonWrapper,FormWrapper) - Styled MUI components:
Styled[Component](e.g.,StyledButton,StyledCard) - Semantic elements: Use semantic names (e.g.,
Header,Footer,Sidebar)
Since there are 792 files using makeStyles, we recommend an incremental approach:
- Install MUI v5 alongside v4 (they can coexist)
- Update
ThemeManager.tsxto support both - Create shared styled components in
core/elements/
- Migrate
core/components/one by one - Update tests as needed
- Migrate plugin components by feature area
- Prioritize actively developed areas
- Remove MUI v4 packages
- Remove any compatibility shims
MUI provides codemods to help automate some migrations:
npx @mui/codemod v5.0.0/preset-safe src/This handles:
- Import path updates
- Component prop renames
- Some
makeStylestostyledconversions
When migrating tests, update mock patterns:
Before:
jest.mock('@material-ui/styles', () => ({
...jest.requireActual('@material-ui/styles'),
makeStyles: () => () => ({}),
}))After:
jest.mock('@mui/material/styles', () => ({
...jest.requireActual('@mui/material/styles'),
styled: () => (Component) => Component,
}))Here's a real-world example from Alert.tsx showing how to migrate a component with dynamic styles based on props:
Before (MUI v4):
import { makeStyles } from '@material-ui/styles'
import Theme from 'core/themes/model'
interface AlertProps {
variant?: 'primary' | 'success' | 'warning' | 'error'
maxWidth?: string
}
const useStyles = makeStyles<Theme, Partial<AlertProps>>((theme) => ({
alert: {
backgroundColor: ({ variant }) => theme.components.alert[variant].background,
maxWidth: ({ maxWidth }) => (maxWidth ? maxWidth : 'unset'),
borderTop: ({ variant }) => `1px solid ${theme.components.alert[variant].border}`,
},
alertTitle: {
marginBottom: 10,
},
}))
export default function Alert({ variant = 'primary', maxWidth }: AlertProps) {
const classes = useStyles({ variant, maxWidth })
return <article className={classes.alert}>...</article>
}After (MUI v5):
import { styled } from '@mui/material/styles'
import { ThemeV5 } from 'core/themes/model'
interface AlertProps {
variant?: 'primary' | 'success' | 'warning' | 'error'
maxWidth?: string
}
interface StyledAlertProps {
variant: AlertProps['variant']
maxWidth?: string
}
const StyledAlert = styled('article')<StyledAlertProps>(
({ theme, variant = 'primary', maxWidth }) => ({
backgroundColor: (theme as ThemeV5).components.alert[variant].background,
maxWidth: maxWidth ?? 'unset',
borderTop: `1px solid ${(theme as ThemeV5).components.alert[variant].border}`,
width: '100%',
boxSizing: 'border-box',
padding: 8,
wordBreak: 'break-word',
}),
)
const AlertTitle = styled('h5')({
marginBottom: 10,
})
export default function Alert({ variant = 'primary', maxWidth }: AlertProps) {
return (
<StyledAlert variant={variant} maxWidth={maxWidth}>
...
</StyledAlert>
)
}