10

When the user hovers over a Card component, I'd like to show a button on that component that is otherwise invisible. In CSS, I'd do something like this:

.card:hover my-button {
  display: block;
}

How do I replicate this in the "Material-UI" way?

All the Material-UI tips I found so far suggest something like this to add hover styling, but this applies the styles to the component that is being hovered over and not a different one.

  '&:hover': {
    background: 'blue'
  }
robin-mbg
  • 183
  • 1
  • 1
  • 8

6 Answers6

13

You can do the exact same thing with CSS using the createMuiTheme:

export const theme = createMuiTheme({
  overrides: {
    // For label
    MuiCard: {
      root: {
        "& .hidden-button": {
          display: "none"
        },
        "&:hover .hidden-button": {
          display: "flex"
        }
      }
    }
  }
});

Give the Button inside your Card the className hidden-button and you will get the same thing that you want.

Check it here: https://codesandbox.io/s/mui-theme-css-hover-example-n8ou5

Dekel
  • 60,707
  • 10
  • 101
  • 129
  • 1
    Worked exactly as given. Except I had to use createTheme and not createMuiTheme which appears to be deprecated as of now. I used this to hide the entire CardAction sub-component – dipan66 Sep 29 '21 at 08:50
9

It is not specific to Material UI but a react specific thing. you need a state variable to show/hide your button.

const App = () => {
  const [show, setShow] = useState(false);
  return (
    <Card
      onMouseOver={() => setShow(true)}
      onMouseOut={() => setShow(false)}>
      <CardBody>
        // some content
        {show && <Button>Click</Button>}
      </CardBody>
    </Card>
  );

}
Zohaib Ijaz
  • 21,926
  • 7
  • 38
  • 60
  • 2
    this example is very simple and works great for 1 card. bud let's say you a have a list of cards - then you somehow need to have the state for all cards saved and change the state for each card separately... which sounds like a lot more work – Asped Sep 17 '21 at 11:34
  • 1
    @Asped not really, if you have a list of cards, create a new component that represents a list item and put the state in there. That way the parent (the list) isn't managing the states of children (the list items) -- each child manages it's own `show` state. – heez Jun 08 '22 at 21:10
  • This is not reliable! When moving the cursor over multiple elements with the same logic this can bug out. – bastianwegge Sep 08 '22 at 16:25
  • @bastianwegge why would this bug out? Each card has it's own state management. Each card has it's own onMouseOver and onMouseOut. I would prefer implementing this using CSS, but if you need to manage more states, I just don't see how this would not work. – Radu May 09 '23 at 14:43
  • Hey @Radu, of course you could go ahead and refactor the Card into its own component, then you're right. This example shows an App component, where a global state is defined, which will be used in the Card below. If you duplicate the Card (a thing beginners will do), this will no longer work as expected or described here, thus my comment. Oh and btw I would be interested in your implementation using CSS, why don't you post this as an answer to this question? – bastianwegge May 19 '23 at 09:07
  • I feel like this is an over-complicated solution. Why use react states when just CSS will work? – Colin McGovern Jul 18 '23 at 19:26
3

I faced this problem today and after I discussed it with my mentor I made this result and it worked well for me. but first, let me tell you the disadvantage of using eventListener like onMouseEnter & on MouseLeave that it will cause so many renders.

I gave the (parent component) a movie-card class and the (child component) a movie-card-content class like the following

// movie-card.css
.movie-card-content {
  opacity: 0;
}

.movie-card:hover .movie-card-content {
  opacity: 1;
}

MUI allows you to add className prop so I gave the proper classNames

//MovieCard.jsx (component)
import "./movie-card.css";

function MovieCard () {

  return (
    <Card className="movie-card">
      <CardContent className="movie-card-content">...<CardContent>
    </Card>
  );
}

and that's it

1
import {
  makeStyles
} from '@material-ui/core'

const useStyles = makeStyles(() => ({
  root: {
    "& .appear-item": {
      display: "none"
    },
    "&:hover .appear-item": {
      display: "block"
    }
  }
}))

export default function MakeTextAppearOnHover() {
  const classes = useStyles()
  return (
    <div className = { classes.root }>
      Hello world
      <span className = 'appear-item' >
        Appearing text Go
      </span>
    </div>
  )
}
1

This is a material UI example that displays the sub-element on hover of the parent. I also noticed that using some of the examples above, when using Material UI's makeStyles, the overlay item was flickering a lot when clicked. This solution below does not flicker when sub-element is clicked.

import React from "react"
import { Card, CardActionArea, CardContent, CardMedia } from "@material- 
       ui/core";
import { makeStyles } from "@material-ui/core/styles";

const useStyles = makeStyles(theme => ({
    card: {
        // some styles
    },
    cardAction: {
        position: "relative"
    },
    media: {
        // some styles
    },
    overlay: {
        position: "absolute",
        top: "85px"
    }
}));

const App = () => {
    const classes = useStyles();
    const [show, setShow] = React.useState(false);
    const handleMouseOver = () => {
        setShow(true);
    };
    const handleMouseOut = () => {
        setShow(false);
    };
    return (
      <Card>
          <CardActionArea
          onMouseOver={handleMouseOver}
          onMouseOut={handleMouseOut} className={classes.card} >
              <CardMedia className={classes.media} component="img" >
              // some content
              </CardMedia>
              <CardContent className={classes.overlay} style={{ display: show ? 
               'block' : 'none' }>
               // some content
              </CardContent>
           </CardActionArea>
      </Card>
  );
}
Dotun
  • 11
  • 1
1

If you want to define this purely inside of a styled component instead of using either of createMuiTheme or makeStyles, then try the following.
We will give an id to each child component and reference them when defining the behaviour to implement when we hover over the parent component:

const NameCellBox = styled(Box)(({ theme }) => ({
    ...other styles,
    "&:hover #cellBoxLengthTypo": {
        display: "none",
    },
    "&:hover #cellBoxContentTypo": {
        display: "inline-block",
    },
}));

const CellBoxLengthTypo = styled(Typography)(({ theme }) => ({
    fontFamily: theme.typography.fontFamily,
    fontSize: theme.typography.h5.fontSize,
}));

const CellBoxContentTypo = styled(Typography)(({ theme }) => ({
    display: "none",
    fontFamily: theme.typography.fontFamily,
    fontSize: theme.typography.h5.fontSize,
}));
const items: string[] = ["andrew", "barry", "chris", "debbie"];
return (
    <>
        <NameCellBox>
            <CellBoxLengthTypo id="cellBoxLengthTypo">+{items.length}</CellBoxLengthTypo>
            <CellBoxContentTypo id="cellBoxContentTypo">{items.join(", ")}</CellBoxContentTypo>
        </NameCellBox>
    </>
);

Found it useful for updating a DataGrid cell in Material UI when hover or other event fired.

Oisín Foley
  • 2,317
  • 2
  • 22
  • 29