javascriptreactjsmaterial-uireact-componentjss

Styles being overwritten by Material-UI style


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 components internal style. That way someone using it can adjust margins and padding. However, my components default internal style is overwriting my external style and I would like it to be the other way around.

You can see my external style is lower speceficity

The Details:

I am creating a custom component library built on top of material-ui. I'd like to make the custom components api similar to @material-ui so that our devs will find them easier to use. Each component 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 comment that you think what I'm doing doesn't make sense because of the example.


Solution

  • From https://developer.mozilla.org/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 component (e.g. TestButton) and in the code that uses that component, 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:

    1. TestButton is defined after the code that uses it and therefore after the makeStyles call that is defining styles intended to override styles in TestButton. Since the makeStyles call for gButton occurs first, the corresponding CSS class will be first in the <head> element. In real-world usage though, TestButton (your custom component) would be defined in a separate file and be imported. Since imports have to be at the top, any makeStyles calls at the top level of the imported file will be executed before any makeStyles calls in the file using the imported component.

    2. The makeStyles call for TestButton is not being done at the top level. Instead it is being done inside the TestButton function which means it will be executed when TestButton is rendered instead of when TestButton is imported. Calls to makeStyles should always be at the top level rather than nested within a component function. One other minor issue is the name of the variable returned from makeStyles (i.e. GrangeButtonStyles in your example). Since makeStyles 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: