React Material Card Expands on Hover with CSS

Introduction

Combining CSS with MUI components, we can animate a card that expands on a mouse enter event. The card uses two different constants that have the CSS. When the card expands it will reveal two additional buttons on the bottom. This example is one of many ways to achieve animation with React. A common alternative for animations is Framer Motion.

Libraries Needed

MUI – Material UI v5.4.3

Material UI library for React

npm install @mui/material @mui/lab @emotion/react @emotion/styled

Source Files

GitHub

MUI Card Component

This component renders a MUI card and uses the onMouseEnter() and onMouseLeave() events to expand and collapse the card.

The source for ‘MUICard.js’. Below the source is break down of the component.

import React, {  useState, useEffect } from 'react'
import Card from '@mui/material/Card';
import CardHeader from '@mui/material/CardHeader';
import CardMedia from '@mui/material/CardMedia';
import CardContent from '@mui/material/CardContent';
import CardActions from '@mui/material/CardActions';
import Button from '@mui/material/Button';
import Avatar from '@mui/material/Avatar';
import Box from '@mui/material/Box';
import Divider from '@mui/material/Divider';
import Stack from '@mui/material/Stack';
import Grid from '@mui/material/Grid';
import Badge from '@mui/material/Badge';
import { red } from '@mui/material/colors';
import GroupIcon from '@mui/icons-material/Group';
import FavoriteBorderIcon from '@mui/icons-material/FavoriteBorder';
import PrizeEventsIcon from '@mui/icons-material/EmojiEvents';
import IconButton from '@mui/material/IconButton';
import FavoriteIcon from '@mui/icons-material/Favorite';


export default function MUICard(props) {
  
  const [isLiked, setIsLiked] = useState(false)
  const [isEventOpen, setIsEventOpen] = useState(false)  
  const [isExpandCard, setIsExpandCard] = useState(false)
 

  const expandCard = () => {        
    setIsExpandCard(true)
  }
  
  const collapseCard = () => {    
    setIsExpandCard(false)
  }
        
  const  cardRoot = {    
    minWidth: 320,
    maxHeight: 500,
    marginLeft: '20px',
    transition: 'transform 0.5s' 
  }

  const  cardRootExpand = {    
    position: 'absolute',
    minWidth: 340,
    maxHeight: 520,
    marginLeft: '20px',
    transform: 'scale(1.1)',    
    transition: 'transform 0.5s', 
    zIndex: 99,
  }
  
  return (
    <Card style={isExpandCard ? cardRootExpand : cardRoot} 
          onMouseEnter={() => expandCard()}
          onMouseLeave={() => collapseCard()}>
      <CardHeader
        avatar={         
            <Avatar sx={{ bgcolor: red[500] }} aria-label="event">
              R
            </Avatar>                             
        }

        title={props.data.title}
        subheader={
          <b>Sub Header</b>
        }
      >
      </CardHeader>

      <CardMedia  media={'image'}  />

          <Box sx={{display: "flex",  justifyContent: "center"}}>
              <img src={'/contest.svg'}
                width="240"
                height="240"
                layout="intrinsic"/>
          </Box>

      <CardContent>
        
        
        <b>{isEventOpen && 'Vote Now'}</b>          
        
      </CardContent>
      <CardActions disableSpacing>

        <Stack spacing={3}>

          <Stack spacing={3} direction="row">
            <IconButton aria-label="add to favorites">
            {isLiked ?
              <FavoriteIcon color={'error'}/>
            :
                <FavoriteBorderIcon color={'primary'}/>
            }
            </IconButton>
                                        
            <IconButton aria-label="share">
                <Badge badgeContent={4} color="success">
                <GroupIcon />
                </Badge>              
            </IconButton>
            
                        
            
            <Button  variant="outlined" size="small" color="primary">Open</Button>


        </Stack>

  
          {isExpandCard &&
          <>
            <Divider/>                
            <Grid container spacing={1}>
                <Grid item xs={6} align="left">                        
                    <Button variant='outlined'>Watch Later</Button>
                </Grid>

                <Grid item xs={6} align="right">
                    <Button variant='outlined' color="warning">Add to Queue</Button>
                </Grid>
            </Grid>                        
          </>
          }
        </Stack>        
      </CardActions>
    </Card>    
  );
}

Breakdown of the MUICard.js Component

Track the mouse events with a boolean state variable isExpandCard.

  const [isExpandCard, setIsExpandCard] = useState(false)
 
  const expandCard = () => {        
    setIsExpandCard(true)
  }
  
  const collapseCard = () => {    
    setIsExpandCard(false)
  }

CSS and JS to Animate the Card

Define the CSS for initial render and hover.

const cardRoot = {    
    minWidth: 320,
    maxHeight: 500,
    marginLeft: '20px',
    transition: 'transform 0.5s' 
}

const cardRootExpand = {    
    position: 'absolute',
    minWidth: 340,
    maxHeight: 520,
    marginLeft: '20px',
    transform: 'scale(1.1)',    
    transition: 'transform 0.5s', 
    zIndex: 99
}

Using the onMouseEnter() and onMouseLeave() events to to change the CSS constants above. Using boolean state variable isExpandCard.

<Card style={isExpandCard ? cardRootExpand : cardRoot} 
          onMouseEnter={() => expandCard()}
          onMouseLeave={() => closeCard()}>
    

App Root Component

Below is the source for app.js. This component uses the <Grid/> component to render cards from hard coded JSON data constant with 16 records. The cards are optimized for a 16:9 monitor and wrapped with a MUI <Container/>.

import * as React from 'react';
import Box from '@mui/material/Box';
import Grid from '@mui/material/Grid';
import MUICard from './components/MUICard';
import Stack from '@mui/material/Stack';
import Container from '@mui/material/Container';

function App() {
     
  return (
    <>        
      <Box sx={{mt:5}}>
        <Container maxWidth="xl" >       
           <Grid container spacing={2}>
                {data.map((elem, index) => (
                    <Grid item xs={12} sm={12} md={4} lg={4} xl={3}>                 
                       <MUICard data={elem} key={index}/>
                    </Grid>
                ))}
           </Grid>  
        </Container>
      </Box>
    </>
  );
}

export default App;


const data = [
  {id: 1, title: 'Magnificent Visions'},
  {id: 2, title: 'The Last Snake'},
  {id: 3, title: 'Blade of Year'},
  {id: 4, title: 'The Ways Captive'},
  {id: 5, title: 'Silk in the Abyss'},
  {id: 6, title: 'Growing Star'},
  {id: 7, title: 'The Last Snake'},
  {id: 8, title: 'The Cracked Willow'},
  {id: 9, title: 'Heart of Servant'},
  {id: 10, title: 'The Scent of the Door'},
  {id: 11, title: 'Flames in the End'},
  {id: 12, title: 'Professional Serpents'},
  {id: 13, title: 'Hunter of Storm'},
  {id: 14, title: 'Storm Illusion'},
  {id: 15, title: 'The Abyss'},
  {id: 16, title: 'Sucking Name'},
  {id: 17, title: 'Moon in the Truth'},
  {id: 18, title: 'Silent Rings'},
  {id: 19, title: 'Roses of Vision'},
  {id: 20, title: 'The Crying Room'},
  {id: 21, title: 'Magnificent Visions'},
  {id: 22, title: 'The Last Snake'},
  {id: 23, title: 'Blade of Year'},
  {id: 24, title: 'The Ways Captive'},
  {id: 25, title: 'Silk in the Abyss'},
  {id: 26, title: 'Growing Star'},
  {id: 27, title: 'The Last Snake'},
  {id: 28, title: 'The Cracked Willow'},    
]