How to Create Custom Variants and Slots for Material-UI Components
You can extend Material-UI components by defining custom variants in your theme's components configuration and by replacing internal elements through the slots API using the components and componentsProps props.
Material-UI (MUI) provides two powerful extension mechanisms that let you customize component appearance without forking the library. According to the mui/material-ui source code, custom variants allow you to add new stylistic "flavors" selectable via the variant prop, while slots let you swap or augment internal sub-components like root elements or icons. Both systems leverage the styled foundation in packages/mui-material/src/styles to ensure type safety and theme consistency.
Understanding Custom Variants in Material-UI
Custom variants let you introduce new visual styles that behave like the built-in contained, outlined, or text variants. The variant system lives in packages/mui-material/src/styles/variantUtils.js, where the getVariantStyle function matches your component's props against theme-defined selectors.
How Variant Resolution Works
When a component renders, MUI's styling pipeline calls useThemeProps → getThemeProps → variant handling. The system checks theme.components[ComponentName].variants for an entry whose props selector matches the current component props. When a match is found, the associated style object merges into the final CSS.
In packages/mui-material/src/Button/Button.js, this logic determines which variant styles to apply based on the variant prop value.
Adding a Custom "Danger" Button Variant
Define the variant in your theme by extending the component's variants array:
// theme.ts
import { createTheme } from '@mui/material/styles';
export const theme = createTheme({
components: {
MuiButton: {
variants: [
{
props: { variant: 'danger' },
style: {
color: '#fff',
backgroundColor: '#d32f2f',
'&:hover': {
backgroundColor: '#b71c1c',
},
},
},
],
},
},
});
Then use it in your JSX:
// App.tsx
import { ThemeProvider } from '@mui/material/styles';
import Button from '@mui/material/Button';
import { theme } from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
<Button variant="danger">Delete</Button>
</ThemeProvider>
);
}
Working with Component Slots
Slots refer to the internal sub-components that make up a composite component—such as the root, label, or input elements. MUI exposes these through the components and componentsProps props, allowing you to replace default elements or pass additional props without reimplementing the entire component.
Slot Architecture in the Source
Each component declares its available slots in a slots object. For example, packages/mui-material/src/Card/Card.js defines slots like root that you can target. The resolution logic uses packages/mui-material/src/utils/composeClasses.js (referencing unstable_composeClasses.js) to generate class names, while packages/mui-material/src/utils/appendOwnerState.js merges user-supplied componentsProps with the component's owner state before forwarding.
Replacing the Card Root Slot
Here's how to swap the default Card root element with a styled Paper component:
import Card from '@mui/material/Card';
import Paper from '@mui/material/Paper';
import { styled } from '@mui/material/styles';
const ElevatedPaper = styled(Paper)(({ theme }) => ({
padding: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
}));
function CustomCard() {
return (
<Card
components={{ Root: ElevatedPaper }}
componentsProps={{ root: { elevation: 8 } }}
>
Content here
</Card>
);
}
The components prop maps slot names to replacement components, while componentsProps forwards props to those slots.
Combining Custom Variants with Slots
Custom variants and slots are composable. You can define a variant that targets a specific slot's class name (like & .MuiButton-startIcon), or you can pass a custom variant prop to a slotted component for deeper theming.
Creating a Rounded TextField with Custom Input Slot
This example applies a custom "rounded" variant to an OutlinedInput slot within a TextField:
// theme.ts
export const theme = createTheme({
components: {
MuiOutlinedInput: {
variants: [
{
props: { variant: 'rounded' },
style: {
borderRadius: '24px',
'& .MuiInputBase-input': { padding: '12px 16px' },
},
},
],
},
},
});
import TextField from '@mui/material/TextField';
import InputBase from '@mui/material/InputBase';
import { ThemeProvider } from '@mui/material/styles';
import { theme } from './theme';
function RoundedField() {
return (
<ThemeProvider theme={theme}>
<TextField
variant="outlined"
label="Rounded"
components={{ Input: InputBase }}
componentsProps={{ input: { variant: 'rounded' } }}
/>
</ThemeProvider>
);
}
The packages/mui-material/src/TextField/TextField.js source demonstrates how the component forwards props to its nested Input slot, allowing the custom variant to cascade through the component tree.
Key Implementation Files in the MUI Source
These files in the mui/material-ui repository contain the core logic for variants and slots:
-
packages/mui-material/src/styles/variantUtils.js: ContainsgetVariantStyle, the utility that matchespropsselectors and merges variant styles into the component's CSS. -
packages/mui-material/src/Button/Button.js: Demonstrates how thevariantprop is read and merged with theme-defined variants. -
packages/mui-material/src/Card/Card.js: Shows theslotsobject declaration and thecomponents/componentsPropsAPI implementation. -
packages/mui-material/src/utils/composeClasses.js: Generates class names for each slot, enabling slot-specific styling and composition. -
packages/mui-material/src/utils/appendOwnerState.js: Merges user-suppliedcomponentsPropswith the owner state before forwarding to slot components. -
packages/mui-material/src/TextField/TextField.js: Illustrates how complex components forward custom variants to nested slots.
Summary
- Custom variants are defined in
theme.components[ComponentName].variantsusing apropsselector andstyleblock, processed byvariantUtils.js. - Slots expose internal component parts via the
componentsandcomponentsPropsprops, resolved throughcomposeClasses.jsandappendOwnerState.js. - You can reference actual source files like
Button.jsandCard.jsto understand how MUI implements these patterns. - Combining variants with slots allows you to create reusable component skins that remain type-safe and theme-aware.
Frequently Asked Questions
How do I add a custom variant to a Material-UI Button?
Extend the MuiButton configuration in your theme's components section with a new entry in the variants array. Each variant requires a props object that acts as a selector (e.g., { variant: 'danger' }) and a style object containing the CSS. When you use <Button variant="danger">, MUI's styling engine automatically merges your custom styles via the logic in packages/mui-material/src/styles/variantUtils.js.
What is the difference between components and componentsProps in MUI slots?
The components prop accepts an object mapping slot names to React components, allowing you to replace the default element for that slot (e.g., components={{ Root: CustomPaper }}). The componentsProps prop accepts an object of props to forward to those slots (e.g., componentsProps={{ root: { elevation: 4 } }}). Internally, MUI uses appendOwnerState to merge these with the component's internal state before rendering.
Can I target custom variants when styling component slots?
Yes. Custom variants can target slot-specific class names using the nested selector syntax in your style definition. For example, a custom Button variant can style the startIcon slot using & .MuiButton-startIcon. Additionally, slots themselves can receive custom variants through componentsProps, allowing the slotted component to apply its own themed variations.
Where does the variant style merging logic live in the MUI source code?
The core variant resolution logic resides in packages/mui-material/src/styles/variantUtils.js, specifically in the getVariantStyle function. This utility is called during the styling pipeline (after useThemeProps and getThemeProps) to match the component's current props against the theme's variant definitions and merge the appropriate styles into the final CSS output.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →