[RFC] Support custom variants in the theme
See original GitHub issueSummary
This RFC is proposing a solution for adding custom variants for the core components inside the theme. We already have an option for adding custom overrides inside the theme, with this RFC we want to extend it to support custom variants as well.
Basic example
The API could look something like this:
const theme = outerTheme => createMuiTheme({
variants: {
MuiTypography: [
{
props: { variant: 'headline1' }, // combination of props for which the styles will be applied
styles: {
padding: '5px 15px',
border: `5px dashed ${ ${outerTheme.palette.primary.main}}`,
},
},
{
props: { variant: 'headline1', color: 'secondary' },
styles: {
padding: '5px 15px',
border: `5px dashed ${outerTheme.palette.secondary.main}`,
},
},
],
},
});
declare module '@material-ui/core/Typography/Typography' {
interface TypographyPropsVariantOverrides {
headline1: true;
h1: false; // variant="h1" is no longer available
}
}
<Typography variant="headline1" color="secondary" />
Motivation
From the developer’s survey, the 3rd most popular use case for Material-UI is to build a custom design system on top of it. This proposal is meant to make it easier. Currently developers can add new props combination by creating wrapper components:
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { deepmerge } from "@material-ui/utils";
import MuiButton, {
ButtonProps as MuiButtonProps
} from "@material-ui/core/Button";
type ButtonProps = Omit<MuiButtonProps, "variant"> & {
variant: "text" | "outlined" | "contained" | "dashed";
};
const useStyles = makeStyles(theme => ({
root: ({ variant }: ButtonProps) => ({
...(variant === "dashed" && {
border: "2px dashed grey"
})
})
}));
const Button: React.FC<ButtonProps> = props => {
const variantClassses = useStyles(props);
const { classes: propsClasses, variant: propsVariant, ...rest } = props;
const classes = deepmerge(variantClassses, propsClasses);
const variant = propsVariant !== "dashed" ? propsVariant : undefined;
return <MuiButton classes={classes} variant={variant} {...rest} />;
};
export default function App() {
return (
<Button variant="dashed" color="secondary">
Custom variant
</Button>
);
}
Adding and removing variants from Material-UI components creates a challenge. You have to document these variants as well as making sure they will be used correctly. Solving the issue at the documentation level will likely require making progress on #21111.
While this option is already available, we have heard pushbacks from the community around it.
- #15573: 38 upvotes, proposed solution with wrapper: 0 reactions.
- #15573: “ability to use theme to a greater extent”
- #15454: “prevents you from having to repeat class implementations (or create yet another file in /components/)”
- #8498: “New Typography variants”
The issues with the wrapper path are:
- It’s more effort. You have to 1. write the component, 2. update all the codebase to migrate the usage to the new component, 3. put constraints in place to make sure the other developers on the project will only use the wrapper component. Some teams decide to pay the cost upfront, start by wrapping all the Material-UI components.
- The wrapper component approach doesn’t play nicely with our goal to provide different themes. If we look at how it’s working with jQuery and static templates, the class names are constraints your put in your codebase, token that then can be targeted to apply customization, like a custom Bootstrap theme. You don’t add new class names before reusing the existing ones. Shouldn’t it be the same in the React era? The components could be the new class names. You add them once, you are set.
In the long run, it could be ideal if we can implement the Material Design light and dark themes with this approach alone.
Detailed design
PR https://github.com/mui-org/material-ui/pull/21648 is implementing this feature for the Button
component. This is how it can be used:
import React from 'react';
import {
createMuiTheme,
makeStyles,
ThemeProvider,
} from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
const theme = outerTheme => createMuiTheme({
variants: {
MuiButton: [
{
props: { variant: 'dashed' },
styles: {
padding: '5px 15px',
border: `5px dashed ${ ${outerTheme.palette.primary.main}}`,
},
},
{
props: { variant: 'dashed', color: 'secondary' },
styles: {
padding: '5px 15px',
border: `5px dashed ${outerTheme.palette.secondary.main}`,
},
},
],
},
});
export default function App() {
return (
<ThemeProvider theme={theme}>
<Button variant="dashed" color="secondary">
Custom variant
</Button>
<ThemeProvider>
);
}
The typescript users, can use module augmentation for defining their new variants types:
declare module '@material-ui/core/Button/Button' {
interface ButtonPropsVariantOverrides {
dashed: true;
}
}
Drawbacks
Always with new API we have to consider also the drawbacks of adding it. Here are some points:
- the proposed feature can already be implemented in userspace (with wrapper components)
- clients will need to learn new API, while longer-term, this API could be also used by design tools to customize Material-UI components.
- there may be some necessary changes per component, regarding the
classKey
definition - there has to be per component validation and implementation of the feature
Alternatives
This can be implemented with wrapper components. Another idea that we tried was, relaxing the typings of the overrides
key, and allowing users to specify the new classKeys
directly there - this will mean that clients need to know how the props are converted to classes keys inside each component.
Adoption strategy
As this is a new API, the adoption can be straight forward for the users.
Unresolved questions
One thing that we need to decide on whether to support slots styles inside the defining, for example, defining the styles of the root
and label
slots in the Button
.
const theme = outerTheme => createMuiTheme({
variants: {
MuiButton: [
{
props: { variant: 'dashed' },
styles: {
root: {
padding: '5px 15px',
border: `5px dashed ${ ${outerTheme.palette.primary.main}}`,
},
label: {
color: outerTheme.palette.primary.main;
}
},
},
{
props: { variant: 'dashed', color: 'secondary' },
styles: {
root: {
padding: '5px 15px',
border: `5px dashed ${ ${outerTheme.palette.secondary.main}}`,
},
label: {
color: outerTheme.palette.secondary.main;
},
},
},
],
},
});
This may require changes in the components implementation and adding some new classKeys
that will support this API.
Progress
Here is a list of all components that would benefit from this API. This list will help us track the progress of where the API is implemented.
-
ButtonGroup https://github.com/mui-org/material-ui/pull/22160
-
CircularProgress [Postponed]
-
Drawer [Postponed]
-
FormControl [Postponed]
-
FormHelperText [Postponed]
-
InputAdornment [Postponed]
-
InputLabel [Postponed]
-
LinearProgress [Postponed]
-
Menu [Postponed]
-
MenuList [Postponed]
-
MobileStepper [Postponed]
-
NativeSelect [Postponed]
-
Select [Postponed]
-
SwipeableDrawer [Postponed]
-
TableCell [Postponed]
-
Tabs [Postponed]
-
TextField [Postponed]
-
Typography https://github.com/mui-org/material-ui/pull/22006
-
Pagination https://github.com/mui-org/material-ui/pull/22219
-
PaginationItem https://github.com/mui-org/material-ui/pull/22220
-
TimelineDot https://github.com/mui-org/material-ui/pull/22244
Issue Analytics
- State:
- Created 3 years ago
- Reactions:36
- Comments:38 (30 by maintainers)
Top GitHub Comments
@EliasJorgensen we are discussing this currently with the development of the new design system cc @siriwatknp In some components the
variant
is used for more than styling purposes, so we cannot reuse it. I would propose in the meantime using somedata-*
attribute, for example, something like - https://codesandbox.io/s/jolly-waterfall-6piyj?file=/src/App.tsx or use a wrapper component.Hi. Is it possible to get support for ‘Card’ component too ? At the moment I don’t see this component in the list of WIP