Can you pass custom props to Material-UI v5 `styled()` components? - reactjs

For example, in styled-components you can do the following:
const Div = styled.div`
background: ${props => props.primary ? "palevioletred" : "white"};
color: ${props => props.primary ? "white" : "palevioletred"};
`;
render(
<section>
<Div>Normal</Div>
<Div primary>Primary</Div>
</section>
);
Can you get the same result with Material-UI v5 styled utility without adding overrides to the global theme object like the example in the styled() docs?

Yes!
The most basic example of the above would look like this in MUI v5:
const Div = styled("div")(({ primary }) => ({
backgroundColor: primary ? "palevioletred" : "white",
color: primary ? "white" : "palevioletred"
}));
render (
<section>
<Div>Normal</Div>
<Div primary>Primary!</Div>
<section>
);
However, as the React docs say:
The unknown-prop warning will fire if you attempt to render a DOM element with a prop that is not recognized by React as a legal DOM attribute/property. You should ensure that your DOM elements do not have spurious props floating around.
So MUI gave us the shouldForwardProp option to tell MUI whether it "should forward the prop" to the root node or not. The above example would look like this using that prop:
const Div = styled("div", {
shouldForwardProp: (prop) => prop !== "primary"
})(({ primary }) => ({
backgroundColor: primary ? "palevioletred" : "white",
color: primary ? "white" : "palevioletred"
}));
render (
<section>
<Div>Normal</Div>
<Div primary>Primary!</Div>
<section>
);
Explanation
The second argument to the styled function is an options object, one of the things it accepts is shouldForwardProp, which as the docs say, "Indicates whether the prop should be forwarded to the Component". So to remove the unknown prop warning from the console, we tell it not to pass our custom prop to the DOM element with shouldForwardProp: (prop) => prop !== "primary". Now we destructure this prop in the function call that returns our custom styles, and use it in those styles like we would any other function.
If you want to use the global theme styles here as well, just destructure it along with your custom prop(s), ie ({ primary, otherProp, thirdProp, theme }).
Working codesandbox.
MUI v5 styled API docs

Here is a fully-working MUI v5 TypeScript example where you can pass custom properties to a styled component:
import React from 'react';
import { Button, styled, Typography } from '#mui/material';
const PREFIX = 'NimbusButton';
const classes = {
root: `${PREFIX}-root`,
button: `${PREFIX}-button`
};
interface RootProps {
textColor?: 'primary' | 'secondary';
buttonTextColor?: 'primary' | 'secondary';
}
const Root = styled('div', {
shouldForwardProp: (prop) => prop !== 'textColor' && prop !== 'buttonTextColor',
name: 'MyThemeComponent',
slot: 'Root'
})<RootProps>(({ theme, textColor, buttonTextColor }) => ({
[`& .${classes.root}`]: {
color: textColor ? theme.palette.primary.main : theme.palette.secondary.main
},
[`& .${classes.button}`]: {
color: buttonTextColor ? theme.palette.primary.main : theme.palette.secondary.main
}
}));
type OwnProps = {
textColor: 'primary' | 'secondary';
buttonTextColor: 'primary' | 'secondary';
text?: string;
buttonText: string;
};
const CustomStyledButton: React.FC<OwnProps> = (props) => {
const { textColor, buttonTextColor, text, buttonText } = props;
return (
<Root className={classes.root} textColor={textColor} buttonTextColor={buttonTextColor}>
{text && <Typography variant={'body1'}>{text}</Typography>}
<Button className={classes.button}>{buttonText}</Button>
</Root>
);
};
export default CustomStyledButton;

If you are using TypeScript, I use utility function for it, so I always type prop names correctly:
export const shouldForwardProp = <CustomProps extends Record<string, unknown>>(
props: Array<keyof CustomProps>,
prop: PropertyKey,
): boolean => !props.includes(prop as string);
const MyComponent = styled('div', {
shouldForwardProp: (prop) => shouldForwardProp<MyComponentProps>(['isDisabled', 'bgColor'], prop),
})<MyComponentProps>(({ theme, isDisabled, size, bgColor }) => ({
...

Related

When migrating to Material-UI v5, how to deal with conditional classes?

In the official migration guide, they give the following example of changing the code from JSS (makeStyles) to the new styled mode.
Before:
const useStyles = makeStyles((theme) => ({
background: theme.palette.primary.main,
}));
function Component() {
const classes = useStyles();
return <div className={classes.root} />
}
After:
const MyComponent = styled('div')(({ theme }) =>
({ background: theme.palette.primary.main }));
function App(props) {
return (
<ThemeProvider theme={theme}>
<MyComponent {...props} />
</ThemeProvider>
);
}
This is fine for a single class, but what to do when the code has conditional classes?
e.g.
<main className={classnames(content, open ? contentOpen : contentClosed)}>
{/* content goes here */}
</main>
Here, content, contentOpen, and contentClosed are classes returned from useStyles, but contentOpen and contentClosed are rendered conditionally based on the value of open.
With the new styled method, instead of generating class names we're generating components. Is there a way to elegantly replicate the rendering without resorting to content duplication in the ternary expression?
e.g. we don't want to do something like:
function App(props) {
return (
<ThemeProvider theme={theme}>
{open
? <MyOpenComponent {...props}>{/* content */}</MyOpenComponent>
: <MyClosedComponent {...props}>{/* content */}</MyClosedComponent>
</ThemeProvider>
);
}
There are quite a few possible ways to deal with this. One approach using styled is to leverage props to do dynamic styles rather than trying to use multiple classes.
Here's an example:
import React from "react";
import Button from "#mui/material/Button";
import { styled } from "#mui/material/styles";
const StyledDiv = styled("div")(({ open, theme }) => {
const color = open
? theme.palette.primary.contrastText
: theme.palette.secondary.contrastText;
return {
backgroundColor: open
? theme.palette.primary.main
: theme.palette.secondary.main,
color,
padding: theme.spacing(0, 1),
"& button": {
color
}
};
});
export default function App() {
const [open, setOpen] = React.useState(false);
return (
<StyledDiv open={open}>
<h1>{open ? "Open" : "Closed"}</h1>
<Button onClick={() => setOpen(!open)}>Toggle</Button>
</StyledDiv>
);
}
Here's an equivalent example using TypeScript:
import * as React from "react";
import Button from "#mui/material/Button";
import { styled } from "#mui/material/styles";
const StyledDiv: React.ComponentType<{ open: boolean }> = styled("div")(
({ open, theme }) => {
const color = open
? theme.palette.primary.contrastText
: theme.palette.secondary.contrastText;
return {
backgroundColor: open
? theme.palette.primary.main
: theme.palette.secondary.main,
color,
padding: theme.spacing(0, 1),
"& button": {
color
}
};
}
);
export default function App() {
const [open, setOpen] = React.useState(false);
return (
<StyledDiv open={open}>
<h1>{open ? "Open" : "Closed"}</h1>
<Button onClick={() => setOpen(!open)}>Toggle</Button>
</StyledDiv>
);
}
Some other possible approaches:
Use Emotion's css prop and Emotion's capabilities for composing styles
Use tss-react to retain similar syntax to makeStyles but backed by Emotion (so you wouldn't be including both Emotion and JSS in your bundle as would be the case if you leverage makeStyles from #material-ui/styles). This is the route I took when migrating to v5 and as part of my migration I created a codemod for migrating JSS makeStyles to tss-react's makeStyles.

How to use custom props and theme with material-ui styled components API (Typescript)?

I'm attempting to use the Material UI styled components API to inject both a custom theme, and some props to a particular custom element.
I have either a custom theme or some props working, but not both together
Custom Theme
My custom theme is defined as follows in a separate file...
export interface ITheme extends Theme {
sidebar: SideBarTheme;
}
type SideBarTheme = {
// Various properties
};
const theme = createMuiTheme(
{
// Normal theme modifications
},
{
sidebar: {
// Custom theme modifications
},
} as ITheme
);
Which I then use with the Material-UI styled component API as follows....
const Profile = styled(Button)(({ theme }: { theme: ITheme }) => ({
backgroundColor: theme.sidebar.profile.backgroundColor,
paddingTop: 20,
paddingBottom: 20
}));
This works fine within my jsx with <Profile />.
Props
For props I'm essentially using an exact copy of the example here
interface MyButtonProps {
color: string;
}
const Profile = styled( ({ color, ...other }: MyButtonProps & Omit<ButtonProps, keyof MyButtonProps>) => (
<Button {...other} />
))({
backgroundColor: (props: MyButtonProps) => props.color,
paddingTop: 20,
paddingBottom: 20
});
This works fine within my jsx when using <Profile color="red"/>
How do I get both of these working together with typescript?
I literally tried combining them like below but the props don't pass down...
const Profile = styled( ({ color, ...other }: MyButtonProps & Omit<ButtonProps, keyof MyButtonProps>) => (
<Button {...other} />
))(({ theme }: { theme: ITheme }) => ({
backgroundColor: (props: MyButtonProps) => props.color,
color: theme.sidebar.profile.backgroundColor,
paddingTop: 20,
paddingBottom: 20
}));
Anything else I try just upsets TypeScript. The material-ui styled function is also too complicated for me to understand.
Many thanks,
This problem has also almost drove me crazy, however I've managed to make it work with this approach:
import React from 'react'
import { styled, Theme, withTheme } from '#material-ui/core/styles'
import { Box, BoxProps } from '#material-ui/core'
interface SidebarSectionProps {
padded?: boolean
theme: Theme
}
export const SidebarSectionComponent = styled(
(props: SidebarSectionProps & BoxProps) => <Box {...props} />
)({
marginBottom: ({ theme }: SidebarSectionProps) => theme.spacing(3),
paddingLeft: ({ theme, padded }: SidebarSectionProps) =>
padded ? theme.spacing(3) : 0,
paddingRight: ({ theme, padded }: SidebarSectionProps) =>
padded ? theme.spacing(3) : 0
})
export const SidebarSection = withTheme(SidebarSectionComponent)
For those who is using MUI5 and wondering how this could be done you can find an example in this github comment from material ui.
Copy of Typescript example:
const StyledComp = styled("div", {
shouldForwardProp: (prop) => prop !== "color" && prop !== 'myProp',
})<{ myProp?: boolean; color?: string }>(({ theme, myProp, color }) => ({
backgroundColor: myProp ? "aliceblue" : "red",
color,
padding: theme.spacing(1)
}));
<StyledComp myProp color="red" /> // typed safe
I'm not sure it makes sense to use styled-components or the material styled HOC because those are better for setting fixed styles which don't depend on Props.
But you can set the styles that you want on through the style prop of the Button component.
Here's a solution that seems to work:
interface AddedProps {
theme: Theme;
color: string;
};
type InnerProps = AddedProps & Omit<ButtonProps, keyof AddedProps>
const InnerProfile = ({color, theme, ...other}: InnerProps) => (
<Button
{...other}
style={{
...other.style,
paddingTop: 20,
paddingBottom: 20,
backgroundColor: color,
color: (theme as MyTheme).sidebar?.profile?.backgroundColor,
}}
/>
)
export const Profile = withTheme( InnerProfile );
We say that our Profile needs two AddedProps: the color and the theme. We write InnerProfile assuming that those props exist. Then in a separate step we apply the withTheme HOC so that the theme prop will be set. The color prop is required and must be set by you when using <Profile />.
Now for this wonky craziness:
color: (theme as MyTheme).sidebar?.profile?.backgroundColor
What's happening is that withTheme knows that it injects a theme but it doesn't know that it injects your specific theme so typescript doesn't know that the theme has a property sidebar at all, since that's a custom property that you added. You need to tell typescript "assume this theme has the properties of MyTheme" by doing (theme as MyTheme). You can get that type MyTheme by using typeof on the object where you created your custom theme.
Since I don't have access to your custom theme object, what I did was define an interface that has the needed property path, but with everything optional. That looks like:
interface MyTheme extends Theme {
sidebar?: {
profile?: {
backgroundColor?: string;
}
}
}
And I used typescript ?. notation when accessing sidebar?.profile?.backgroundColor to avoid the "object is possibly undefined" error.

Styling a Select element from react-select

I'm using a select element from the react-select library, and in my project I'm using styled-components.
I wanted to ask if it is possible for me to style it in my styles.ts file. if it's not possible, can you guys give any suggestions of how to do the styling??
Inside a React:FC
import Select from 'react-select'
...
const options = [
{ value: 'Income', label: 'Income' },
{ value: 'Expense', label: 'Expense' },
]
...
<Form>
<InputElement />
<Select options={options} />
</Form>
Yes, it is possible to provide your own custom styles, react-select provides an object-based approach for styling the Select component.
Reference to the docs
Here is a simple example,
const customStyles = {
option: (provided, state) => ({
...provided,
borderBottom: '1px dotted pink',
color: state.isSelected ? 'red' : 'blue',
padding: 20,
}),
control: () => ({
// none of react-select's styles are passed to <Control />
width: 200,
}),
singleValue: (provided, state) => {
const opacity = state.isDisabled ? 0.5 : 1;
const transition = 'opacity 300ms';
return { ...provided, opacity, transition };
}
}
const App = () => (
<Select
styles={customStyles} // pass the customStyles object to the styles prop
options={...}
/>
);
Select is very customizable through the keys provided to the custom style object.
One thing to remember is that each key will be a style function where the first argument will be the provided styles and the second argument will the state of select i.e isFocused, isSelected.
EDIT- While it is the suggested way of providing custom styles with the object-based approach. If you really want to style the Select component with styled-components, here is an example (you have to provide a classNamePrefix through prop and style based on classNames)
More on details on styling with classNames docs
import Select from 'react-select';
import styled from 'styled-components';
const SelectElement = styled(Select)`
.react-select-container {
// custom styles
}
.react-select__control {
// custom styles
}
.react-select__indicators {
// custom styles
}
`;
const MySelect = (props) => {
return (
<SelectElement classNamePrefix="react-select" options={options} {...props}/>
)
}
Yes, you can do it like this
import ReactSelect from 'react-select';
import styled from 'styled-components';
const ReactSelectStyled = styled(ReactSelect)`
// you have to provide custom styles through class names
// example
.react-select__option {
// custom styles
}
`;
Additionally, you can change the prefix of the class name (i.e "react-select" part in the class names) by providing the prefix through the classNamePrefix prop to the Select component.

Avoid passing down props using Styled Components and Typescript

I have the following Styled Component in a React app that works as expected:
const BaseButton = styled(Button)<{ borderColor: string }>`
border-color: ${({ borderColor }): string => borderColor};
`;
However it generates this warning message in the console:
React does not recognize the borderColor prop on a DOM element. If
you intentionally want it to appear in the DOM as a custom attribute,
spell it as lowercase bordercolor instead. If you accidentally
passed it from a parent component, remove it from the DOM element.
In order to avoid this, I've tried to implement the solution proposed in the documentation
Documentation example:
import styled from 'styled-components'
import Header, { Props as HeaderProps } from './Header'
const Title =
styled <
{ isActive: boolean } >
(({ isActive, ...rest }) => <Header {...rest} />)`
color: ${props => (
props.isActive ? props.theme.primaryColor : props.theme.secondaryColor
)}
`
My original code rewritten following the example:
const BaseButton = styled <
{ borderColor: string } >
(({ borderColor, ...rest }) => <Button {...rest} />)`
border-color: ${({ borderColor }): string => borderColor};
`;
But I get the following error message:
Parsing error: '>' expected
The error refers to <Button {rest...} />
This is my .babelrc config in case something is amiss:
{
"presets": ["#babel/env", "#babel/typescript", "#babel/preset-react"],
"plugins": [
"#babel/plugin-proposal-object-rest-spread",
"#babel/transform-runtime",
"#babel/plugin-transform-modules-commonjs"
]
}
Use Transient props
TL;DR just prefix your attriibute with $ sign. example $borderColor, $black, $any, $attribute.
If you want to prevent props meant to be consumed by styled components from being passed to the underlying React node or rendered to the DOM element, you can prefix the prop name with a dollar sign ($), turning it into a transient prop.
// typescript example
const BaseButton = styled(Button)<{ $borderColor: string }>`
border-color: ${({ $borderColor }): string => $borderColor};
`;
// js
const BaseButton = styled(Button)`
border-color: ${({$borderColor}) => $borderColor}
`;
// usage
<BaseButton $borderColor="red">Button</BaseButton>
2nd method
Checkout shouldForwardProp
const Comp = styled('div').withConfig({
shouldForwardProp: (prop, defaultValidatorFn) =>
!['hidden'].includes(prop)
&& defaultValidatorFn(prop),
}).attrs({ className: 'foo' })`
color: red;
&.foo {
text-decoration: underline;
}
`;
render(
<Comp hidden draggable="true">
Drag Me!
</Comp>
);
Your existing code was already right, but react gave you two options :
1) use lower case than snake-case
2) remove the attribute from DOM (you took this approach)
From the code I can see that you need the prop borderColor, but in the custom styling, you separated the props
({borderColor,... rest}) => <Button {...rest} />
You removed the border Color prop but you try to access in styled props the next line.
Instead try to rename the prop to bordercolor if you want warning to go away or just ignore warning.

How to extend styled component without passing props to underlying DOM element?

I have a styled component that is extending a third-party component:
import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.someProp}px;
`;
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
someProp={someProp}
>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
This resulted in the following error in the browser console:
Warning: React does not recognize the someProp prop on a DOM
element. If you intentionally want it to appear in the DOM as a custom
attribute, spell it as lowercase someProp instead. If you
accidentally passed it from a parent component, remove it from the DOM
element
So, I changed the prop to have a data-* attribute name:
import Resizable from 're-resizable';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props['data-s']}px;
`;
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
data-s={someProp}
>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
This works, but I was wondering if there was a native way to use props in the styled component without them being passed down to the DOM element (?)
2020 UPDATE: Use transient props
With the release 5.1.0 you can use transient props. This way you do not need an extra wrapper i.e. unnecessary code is reduced:
Transient props are a new pattern to pass props that are explicitly consumed only by styled components and are not meant to be passed down to deeper component layers. Here's how you use them:
const Comp = styled.div`
color: ${props => props.$fg || 'black'};
`;
render(<Comp $fg="red">I'm red!</Comp>);
Note the dollar sign ($) prefix on the prop; this marks it as transient and styled-components knows not to add it to the rendered DOM element or pass it further down the component hierarchy.
The new answer should be:
styled component:
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.$someProp}px;
`;
parent component:
const PaneContainer = ({ children, someProp }) => (
<StyledPaneContainer
$someProp={someProp} // '$' signals the transient prop
>
{children}
</StyledPaneContainer>
);
As suggested by vdanchenkov on this styled-components github issue you can destructure the props and only pass the relevant ones to Resizable
const ResizableSC = styled(({ someProp, ...rest }) => <Resizable {...rest} />)`
flex: 0 0 ${(props) => props.someProp}px;
`;
For those (like me) that landed here because of an issue with SC and react-router's Link.
This is basically the same answer as #tskjetne, but with JS syntax style.
const StyledLink = styled(({ isCurrent, ...rest }) => <Link {...rest} />)(
({ isCurrent }) => ({
borderBottomColor: isCurrent ? 'green' : 'transparent',
}),
)
Starting with version 5.1, you can use shouldForwardProp configuration property:
This is a more dynamic, granular filtering mechanism than transient props. It's handy in situations where multiple higher-order components are being composed together and happen to share the same prop name. shouldForwardProp works much like the predicate callback of Array.filter. A prop that fails the test isn't passed down to underlying components, just like a transient prop.
Keep in mind that, as in this example, other chainable methods should always be executed after .withConfig.
I find it much cleaner than transient props, because you don't have to rename a property, and you become explicit about your intentions:
const ResizableSC = styled(Resizable).withConfig({
// Filter out the props to not be shown in DOM.
shouldForwardProp: (prop, defaultValidatorFn) =>
prop !== 'someProp'
&& defaultValidatorFn(prop),
})`
flex: 0 0 ${(props) => props.someProp}px;
`;
This is especially handy if you are using TypeScript and sharing the same props type both in your main component and the corresponding styled component:
import { HTMLAttributes } from 'react';
import styled from 'styled-components';
// Props type.
type CaptionProps = HTMLAttributes<HTMLParagraphElement> & {
size: number,
};
// Main component.
export const CaptionStyles = styled('p').withConfig<CaptionProps>({
// Filter out the props to not be shown in DOM.
shouldForwardProp: (prop, defaultValidatorFn) => (
prop !== 'size'
&& defaultValidatorFn(prop)
),
})`
flex: 0 0 ${(props) => props.size}px;
`;
// Corresponding styled component.
export function Caption({ size }: CaptionProps) {
return (
<CaptionStyles size={size} />
);
}
shouldForwardProp API reference
You can try with defaultProps:
import Resizable from 're-resizable';
import PropTypes from 'prop-types';
...
const ResizableSC = styled(Resizable)``;
export const StyledPaneContainer = ResizableSC.extend`
flex: 0 0 ${(props) => props.someProp}px;
`;
StyledPaneContainer.defaultProps = { someProp: 1 }
const PaneContainer = ({ children }) => (
<StyledPaneContainer>
{children}
</StyledPaneContainer>
);
export default PaneContainer;
We can also pass props using 'attrs'. This helps in attaching additional props (Example taken from styled components official doc):
const Input = styled.input.attrs({
// we can define static props
type: 'password',
// or we can define dynamic ones
margin: props => props.size || '1em',
padding: props => props.size || '1em'
})`
color: palevioletred;
font-size: 1em;
border: 2px solid palevioletred;
border-radius: 3px;
/* here we use the dynamically computed props */
margin: ${props => props.margin};
padding: ${props => props.padding};
`;
render(
<div>
<Input placeholder="A small text input" size="1em" />
<br />
<Input placeholder="A bigger text input" size="2em" />
</div>
);

Resources