Preface
I asked a similar question to this several days back and while related in nature I believe the solution will ultimately be different, so I am asking again in a different thread.
CodeSanbox Example (Has Been updated to reflect the accepted answer)
The issue:
I'd like any external styles passed in with the className
prop to have higher specificity than my custom ponents internal style. That way someone using it can adjust margins and padding. However, my ponents default internal style is overwriting my external style and I would like it to be the other way around.
The Details:
I am creating a custom ponent library built on top of material-ui. I'd like to make the custom ponents api similar to @material-ui
so that our devs will find them easier to use. Each ponent I am building has it's own internal style overwriting the default material-ui styles in this case it is defined as class button
. Additionally, like @material-ui
I am accepting a color prop <TestButton color={'default'}/>
. Finally, I'd like my custom button to be allowed to be overwritten with external styles if the need ever arises. I am using the clsx
library to build the className strings.
The Code:
import React, { useState } from "react";
import { makeStyles } from "@material-ui/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import { Button } from "@material-ui/core";
import clsx from "clsx";
const useAppStyles = makeStyles({
gButton: { margin: "150px" }
});
export default function App() {
const classes = useAppStyles();
return (
<div className={classes.example}>
<div className={classes.separator}>
<div>Buttons:</div>
<TestButton
className={classes.gButton}
color={"default"}
>
Default
</TestButton>
<TestButton
className={classes.gButton}
color={"primary"}
>
Primary
</TestButton>
</div>
);
}
function TestButton(props) {
const classes = GrangeButtonStyles();
let color = props.color === 'default' ? classes.default : classes.primary
const GrangeButtonStyles = makeStyles({
button: {
height: "45px",
padding: "13px 30px 13px 30px",
borderRadius: "5px",
border: "none",
margin: "15px",
},
default: {
backgroundColor: "black",
border: 'solid #2e7d32 1px',
color: "white",
},
primary: {
backgroundColor: 'white',
color: 'black',
fontFamily: 'Montserrat, sans-serif',
border: 'solid black 1px',
}
});
return (
<Button
className={clsx(classes.button, color, props.className)}
variant="contained"
disabled={props.disabled}
disableElevation
>
{props.children}
</Button>
);
}
NOTE:
I have simplified the code greatly for space in this question and in the code sandbox example. Please don't ment that you think what I'm doing doesn't make sense because of the example.
Preface
I asked a similar question to this several days back and while related in nature I believe the solution will ultimately be different, so I am asking again in a different thread.
CodeSanbox Example (Has Been updated to reflect the accepted answer)
The issue:
I'd like any external styles passed in with the className
prop to have higher specificity than my custom ponents internal style. That way someone using it can adjust margins and padding. However, my ponents default internal style is overwriting my external style and I would like it to be the other way around.
The Details:
I am creating a custom ponent library built on top of material-ui. I'd like to make the custom ponents api similar to @material-ui
so that our devs will find them easier to use. Each ponent I am building has it's own internal style overwriting the default material-ui styles in this case it is defined as class button
. Additionally, like @material-ui
I am accepting a color prop <TestButton color={'default'}/>
. Finally, I'd like my custom button to be allowed to be overwritten with external styles if the need ever arises. I am using the clsx
library to build the className strings.
The Code:
import React, { useState } from "react";
import { makeStyles } from "@material-ui/styles";
import InputLabel from "@material-ui/core/InputLabel";
import MenuItem from "@material-ui/core/MenuItem";
import FormControl from "@material-ui/core/FormControl";
import { Button } from "@material-ui/core";
import clsx from "clsx";
const useAppStyles = makeStyles({
gButton: { margin: "150px" }
});
export default function App() {
const classes = useAppStyles();
return (
<div className={classes.example}>
<div className={classes.separator}>
<div>Buttons:</div>
<TestButton
className={classes.gButton}
color={"default"}
>
Default
</TestButton>
<TestButton
className={classes.gButton}
color={"primary"}
>
Primary
</TestButton>
</div>
);
}
function TestButton(props) {
const classes = GrangeButtonStyles();
let color = props.color === 'default' ? classes.default : classes.primary
const GrangeButtonStyles = makeStyles({
button: {
height: "45px",
padding: "13px 30px 13px 30px",
borderRadius: "5px",
border: "none",
margin: "15px",
},
default: {
backgroundColor: "black",
border: 'solid #2e7d32 1px',
color: "white",
},
primary: {
backgroundColor: 'white',
color: 'black',
fontFamily: 'Montserrat, sans-serif',
border: 'solid black 1px',
}
});
return (
<Button
className={clsx(classes.button, color, props.className)}
variant="contained"
disabled={props.disabled}
disableElevation
>
{props.children}
</Button>
);
}
NOTE:
I have simplified the code greatly for space in this question and in the code sandbox example. Please don't ment that you think what I'm doing doesn't make sense because of the example.
Share Improve this question edited Feb 12, 2020 at 16:08 Jonny B asked Feb 12, 2020 at 0:55 Jonny BJonny B 7202 gold badges12 silver badges30 bronze badges 17- Check out the drastic difference by the few changes I made here: codesandbox.io/s/material-demo-l0hn1 – Adam Jenkins Commented Feb 12, 2020 at 1:29
-
The difference in specificity between the styles from different
makeStyles
calls is dependent on the order of the style sheets in the<head>
. This is determined by the order in whichmakeStyles
is called. Later calls will be later in the<head>
and thus have higher specificity. If you put your custom ponents in files of their own, theirmakeStyles
will generally be called first due to importing them before using them and the specificity should be as desired. – Ryan Cogswell Commented Feb 12, 2020 at 1:33 - You can find related information here and here. – Ryan Cogswell Commented Feb 12, 2020 at 1:34
- @Adam, Your changes just look like they pletely blow away the internal styles of the select. I'm not sure of the point though. Sorry. – Jonny B Commented Feb 12, 2020 at 1:37
-
1
@RyanCogswell, your solution of splitting it up was the correct solution. Further, the reason it wasn't working in my library was that I was calling
makeStyles
from within the button ponent itself. Seems that it would then call the internalmakeStyles
after the internal ponentmakeStyles
. I appreciate your help and you input on the load order ofmakeStyles
. Would submit that advise as an answer? I'd like to give you the credit for it. – Jonny B Commented Feb 12, 2020 at 14:06
2 Answers
Reset to default 3From https://developer.mozilla/en-US/docs/Web/CSS/Specificity:
When multiple declarations have equal specificity, the last declaration found in the CSS is applied to the element.
So in your case where you are defining CSS classes in your custom ponent (e.g. TestButton
) and in the code that uses that ponent, the specificity is determined by the order in which those CSS classes appear within the <head>
element. This order is determined by an index that is set when makeStyles is called, so classes defined by later calls to makeStyles
will appear later in the <head>
element and thus have greater specificity.
There are two issues then in your example:
TestButton
is defined after the code that uses it and therefore after themakeStyles
call that is defining styles intended to override styles inTestButton
. Since themakeStyles
call forgButton
occurs first, the corresponding CSS class will be first in the<head>
element. In real-world usage though,TestButton
(your custom ponent) would be defined in a separate file and be imported. Since imports have to be at the top, anymakeStyles
calls at the top level of the imported file will be executed before anymakeStyles
calls in the file using the imported ponent.The
makeStyles
call forTestButton
is not being done at the top level. Instead it is being done inside theTestButton
function which means it will be executed whenTestButton
is rendered instead of whenTestButton
is imported. Calls tomakeStyles
should always be at the top level rather than nested within a ponent function. One other minor issue is the name of the variable returned frommakeStyles
(i.e.GrangeButtonStyles
in your example). SincemakeStyles
returns a custom hook, you should always have a name that starts with "use" (e.g.useGrangeButtonStyles
). This will ensure that the eslint rules for hooks recognize it as a hook and warn you of any hook misuse.
Related answers and references:
- Material UI v4 makeStyles exported from a single file doesn't retain the styles on refresh
- Internal implementation of "makeStyles" in React Material-UI?
- https://developer.mozilla/en-US/docs/Web/CSS/Specificity
<TestButton className={classes.gButton} color={"default"}>
// should be
<TestButton classes={{button:classes.gButton}} color={"default"}>
// and
<TestSelect className={classes.gSelect}
// should be
<TestSelect className={{dropdown:classes.gSelect}}
^ when dealing with material-ui's styling solution, don't pass "className" to ponents (only put this prop on DOM elements!!!!)
and
function TestButton(props) {
const classes = GrangeButtonStyles();
...
// should be
function TestButton(props) {
const classes = GrangeButtonStyles(props);
...
^ This will cause the prop classes.button (which will look like jss-1233
) to be merged with the button class that es out of GrangeButtonStyles
so classes will now look like this:
{
button: 'jss-7382 jss-1233' <- jss-1233 is the classname that got generated in the previous ponent
}
and
<Button
className={clsx(classes.button, color, props.className)}
// should be
<Button
classes={{root:classes.button)}
^ See material-ui docs for button
It's actually unfortunate that material-ui forwards it's refs to the DOM element without checking for className because this allows people to put className on material-ui ponents and it "kind of" work. They should really add warnings to use
Thanks for a mentor for correcting my mistake, I still like the verbosity of passing classes
instead.classes
instead of className as mixing the two can result in confusion!
EDIT:
I also noticed you're messing up the styles in TestSelect
- always keep this in mind - don't pass className
as props to material-ui ponents, only pass classes
. If you want to pass styles from a parent ponent to a child ponent then you've got to use the same key:
Let's try an example, I know, this stuff is hard to grok but eventually it will "click":
const useParentStyles = makeStyles({
childStyles: { ... some jss }
childStyles2: { ... some jss }
});
const Parent = props => {
const classes = useParentStyles(props);
return <Child classes={{root:classes.childStyles,someOtherKey:classes.childStyles2}}/> <- object with a keys of "root" and "someOtherKey"
}
const useChildStyles = makeStyles({
root: { ... some jss } <- root
someOtherKey: { ... some jss } <- someOtherKey
});
const Child = props => {
const classes = useChildStyles(props); <- classes have been merged together
return <div className={classes.root}>...</div>
}