Material UI: Display sub-element on hover of parent - reactjs

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'
}

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

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>
);
}

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.

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 😉

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>
)
}

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>
);
}

Related

dark mode toggle in material ui

I am trying to implement a dark toggle in my website. I have gotten the toggle to show up in the correct place. I am using the usestate hook to implement the toggle functionality. However, on clicking the toggle, does not change the theme. I don't understand what is going wrong over here.
Here is the code of the different components.
I have implemented a component for the toggle theme button. Here is the code for togglethemebutton.js
import React, { useState } from "react";
// ----------------------------------------------------------------------
import { Switch } from '#mui/material';
import { createTheme } from '#mui/material/styles';
// ----------------------------------------------------------------------
export default function ToggleThemeButton() {
const [darkState, setDarkState] = useState(false);
const palletType = darkState ? "dark" : "light";
const darkTheme = createTheme({
palette: {
type: palletType,
}
});
const handleThemeChange = () => {
setDarkState(!darkState);
};
return (
<Switch checked={darkState} onChange={handleThemeChange} />
);
}
I am using this component in the Navbar.js component.
import PropTypes from 'prop-types';
// material
import { alpha, styled } from '#mui/material/styles';
import { Box, Stack, AppBar, Toolbar, IconButton } from '#mui/material';
// components
import Iconify from '../../components/Iconify';
//
import Searchbar from './Searchbar';
import AccountPopover from './AccountPopover';
import LanguagePopover from './LanguagePopover';
import NotificationsPopover from './NotificationsPopover';
import ToggleThemeButton from './ToggleThemeButton';
// ----------------------------------------------------------------------
const DRAWER_WIDTH = 280;
const APPBAR_MOBILE = 64;
const APPBAR_DESKTOP = 92;
const RootStyle = styled(AppBar)(({ theme }) => ({
boxShadow: 'none',
backdropFilter: 'blur(6px)',
WebkitBackdropFilter: 'blur(6px)', // Fix on Mobile
backgroundColor: alpha(theme.palette.background.default, 0.72),
[theme.breakpoints.up('lg')]: {
width: `calc(100% - ${DRAWER_WIDTH + 1}px)`
}
}));
const ToolbarStyle = styled(Toolbar)(({ theme }) => ({
minHeight: APPBAR_MOBILE,
[theme.breakpoints.up('lg')]: {
minHeight: APPBAR_DESKTOP,
padding: theme.spacing(0, 5)
}
}));
// ----------------------------------------------------------------------
DashboardNavbar.propTypes = {
onOpenSidebar: PropTypes.func
};
export default function DashboardNavbar({ onOpenSidebar }) {
return (
<RootStyle>
<ToolbarStyle>
<IconButton
onClick={onOpenSidebar}
sx={{ mr: 1, color: 'text.primary', display: { lg: 'none' } }}
>
<Iconify icon="eva:menu-2-fill" />
</IconButton>
<Searchbar />
<Box sx={{ flexGrow: 1 }} />
<Stack direction="row" alignItems="center" spacing={{ xs: 0.5, sm: 1.5 }}>
// here is the toggle button
<ToggleThemeButton/>
<LanguagePopover />
<NotificationsPopover />
<AccountPopover />
</Stack>
</ToolbarStyle>
</RootStyle>
);
}
A posible solution may be to move the logic to change the theme from the ToggleThemButton component to the NavBar and just pass the values needed like this:
ToggleThemeButton:
export default function ToggleThemeButton({handleThemeChange, darkState}) {
return (
<Switch checked={darkState} onChange={handleThemeChange} />
);
}
Then in the NavBar component you could add a theme variable that its updated by the ToggleThemeButton, here i used a new createTheme with the pallete that has a background property just for testing (i don't know much about material ui)
Navbar:
export default function DashboardNavbar({ onOpenSidebar }) {
const [darkState, setDarkState] = useState(false);
const palletType = darkState ? "dark" : "light";
const darkTheme = createTheme({
palette: {
type: palletType,
background: {
default: "#fff"
}
}
});
const [theme, setTheme] = useState(darkTheme);
const handleThemeChange = () => {
setDarkState(!darkState);
setTheme(createTheme({
palette: {
type: palletType,
background: {
default: !darkState? "#000" : "#fff"
}
}
}))
};
return (
<RootStyle theme={theme}>
..... <ToggleThemeButton handleThemeChange={handleThemeChange} darkState={darkState} /> ....
</ToolbarStyle>
</RootStyle>
);
}

Styled MUI Drawer not opening when variant is temporary

I've created a styled MuiDrawer component so I can add some custom styling the component. I want to use the temporary variant but the Drawer is not opening. When I set the Drawer variant to permanent the Drawer does show. So it's probably the passing of the open prop that is causing the error. When I use the default Drawer component from MUI the temporary variant does work:
// demo.tsx
import * as React from 'react';
// import Drawer from '#mui/material/Drawer';
import {Drawer} from './styles';
import Button from '#mui/material/Button';
export default function TemporaryDrawer() {
const [open, setOpen] = React.useState(false);
const toggleDrawer = () => {
setOpen(!open);
};
return (
<>
<Button onClick={toggleDrawer}>Toggle Drawer</Button>
<Drawer
variant='temporary'
open={open}
onClose={toggleDrawer}
>
<p>Drawer</p>
</Drawer>
</>
);
}
// styles.tsx
import {styled} from '#mui/material';
import {Theme, CSSObject} from '#mui/material/styles';
import MuiDrawer from '#mui/material/Drawer';
const drawerWidth = 240;
const openedMixin = (theme: Theme): CSSObject => ({
backgroundColor: 'green',
width: drawerWidth,
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.enteringScreen,
}),
overflowX: 'hidden',
});
const closedMixin = (theme: Theme): CSSObject => ({
transition: theme.transitions.create('width', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
overflowX: 'hidden',
width: `calc(${theme.spacing(7)} + 1px)`,
[theme.breakpoints.up('sm')]: {
width: `calc(${theme.spacing(8)} + 1px)`,
},
});
export const Drawer = styled(MuiDrawer, {shouldForwardProp: (prop) => prop !== 'open'})(
({theme, open}) => ({
width: drawerWidth,
flexShrink: 0,
whiteSpace: 'nowrap',
boxSizing: 'border-box',
...(open && {
...openedMixin(theme),
'& .MuiDrawer-paper': openedMixin(theme),
}),
...(!open && {
...closedMixin(theme),
'& .MuiDrawer-paper': closedMixin(theme),
}),
}),
)
https://codesandbox.io/s/temporarydrawer-material-demo-forked-zci40?file=/demo.tsx
While #v1s10n_4 answer is correct, I'll explain a bit more the reason.
The purpose of the shouldForwardProp callback is to prevent the styling props created by the HOC from leaking to the DOM element leading to invalid attribute error. Your Drawer has an open prop, it's a known prop of Dialog so you don't need to be concerned about the prop is not handled properly here:
const Dialog = (props) => {
// I know this open prop, so I'm gonna extract it here
const { open, ...other } = props
// and do some logic with the open prop
// and make sure it is not passed to the Component
return <Component {...other} />
}
However, if you pass an arbitrary prop that is not from the Dialog API, like bgcolor for example:
<Dialog bgcolor='red'
Then it will be passed down to the DOM element:
const Dialog = (props) => {
const { open, ...other /* other includes the bgcolor prop */ } = props
// logic...
return <Component {...other} />
}
When you are using styled to create a styled component:
const StyledDialog = styled(Dialog)(...)
<StyledDialog bgcolor='red'
It'd look like this behind the scene:
const StyledDialog = (props) => {
const className = getStyles(props);
return <Dialog {...props} className={className} />
}
That's why you need to use shouldForwardProp, to filter out the style-related props (not the Dialog props) so it won't be passed down to the Dialog:
const StyledDialog = (props) => {
const { bgcolor, ...other } = props;
const className = getStyles(props);
// bgcolor is filtered now.
return <Dialog {...other} className={className} />
}
you can remove {shouldForwardProp: (prop) => prop !== 'open'} in your styled Drawer definition.
codesandbox

Material UI pagination - How Can I use custom style for number of pages?

I'm quite new to material-ui. I'm trying to build this component.
I was able to do the style for the next and previous buttons the same as in the picture.
The normal style shows the number of pages as a numbered group besides each other like this:
Are there any properties that I can pass for the pagination component, in which I can change the style?
Here is the code:
import Pagination from "#material-ui/lab/Pagination";
import useStyles from "./styles";
const ReviewsPagination = () => {
const classes = useStyles();
return (
<div className={classes.root}>
<Pagination count={8} />
</div>
);
};
export default ReviewsPagination;
and the style file:
import { makeStyles } from "#material-ui/core/styles";
const useStyles = makeStyles({
root: {
"& .MuiPagination-ul": {
"& > li:first-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
"& > li:last-child": {
"& button": {
borderRadius: "50%",
border: "1px solid black",
width: "48px",
height: "48px",
},
},
},
},
});
export default useStyles;
Thank you!
you can use the usePagination hook to customize the pagination component. Like below:
export default function UsePagination() {
const classes = useStyles();
const { items } = usePagination({
count: 10,
});
return (
<nav>
<ul className={classes.ul}>
{items.map(({ page, type, selected, ...item }, index) => {
let children = null;
if (type === 'next' || type === 'previous') {
children = (
<button type="button" {...item}>
{type}
</button>
);
}else if(selected){
children = <div>{`${page}/10`}</div>
}
return <li key={index}>{children}</li>;
})}
</ul>
</nav>
);
}

React GridList w/ Modal Images

Well, how to start, hi!
I'm creating some Slider with images using Material-UI GridList, and I want those images to be opened in a modal way, just to see them clearly.
I will put the code, and then explain.
import React, {useState} from "react";
import { makeStyles } from "#material-ui/core/styles";
import Modal from "#material-ui/core/Modal";
import tileData from './../../utils/tileData'
import useStylesForSlider from './../../hooks/useStylesForSlider'
import GridList from '#material-ui/core/GridList'
function getModalStyle() {
const top = 50
const left = 50
return {
top: `${top}%`,
left: `${left}%`,
transform: `translate(-${top}%, -${left}%)`
};
}
const useStyles = makeStyles((theme) => ({
paper: {
position: "absolute",
width: 400,
backgroundColor: theme.palette.background.paper,
border: "2px solid #000",
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3)
}
}));
export default function SimpleModal() {
// I have this in another folder, i will put the other ones too, i'm just starting
const classesRoot = useStylesForSlider()
const classes = useStyles();
// getModalStyle is not a pure function, we roll the style only on the first render
const [modalStyle] = useState(getModalStyle);
const [open, setOpen] = useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
const body = (
<div style={modalStyle} className={classes.paper}>
</div>
);
return (
<div className={classesRoot.root}>
<GridList className={classesRoot.gridList} cols={2.5}>
{tileData.map((tile) => (<img src={tile.img} alt={tile.img} onClick={handleOpen} key={tile.img}/>))}
</GridList>
<Modal
open={open}
onClose={handleClose}>
{body}
</Modal>
</div>
);
}
I have in "tileData" an array with the images, and I map them into a tag just to put them in the slider (GridList). It works well. Now, i want to click some img, and then open it in modal window. I click it, and the modal opens, but now comes my question, how do I put the image I clicked somewhere in the "body" constant, or how do I do to do it well. I don't know if i'm explaining well, but I expect to have some good advices, i'm pretty new in React world
You could create a state for the current chosen image index (or a unique id) of your tileData array and then load the image in the body by its index (or id). Here is an example:
import React, { useState } from "react";
import { makeStyles } from "#material-ui/core/styles";
import Modal from "#material-ui/core/Modal";
import tileData from './../../utils/tileData'
import GridList from "#material-ui/core/GridList";
function getModalStyle() {
const top = 50;
const left = 50;
return {
top: `${top}%`,
left: `${left}%`,
transform: `translate(-${top}%, -${left}%)`
};
}
const useStyles = makeStyles((theme) => ({
paper: {
position: "absolute",
width: 400,
backgroundColor: theme.palette.background.paper,
border: "2px solid #000",
boxShadow: theme.shadows[5],
padding: theme.spacing(2, 4, 3)
}
}));
export default function SimpleModal() {
// I have this in another folder, i will put the other ones too, i'm just starting
//const classesRoot = useStylesForSlider();
const classes = useStyles();
// getModalStyle is not a pure function, we roll the style only on the first render
const [modalStyle] = useState(getModalStyle);
const [open, setOpen] = useState(false);
const [currentIdx, setCurrentIdx] = useState(null); // add a state for the current index
const handleOpen = (idx) => {
setCurrentIdx(idx); // set new current index
setOpen(true);
};
const handleClose = () => {
setCurrentIdx(null); // reset current index
setOpen(false);
};
const body = (
<div style={modalStyle} className={classes.paper}>
{tileData[currentIdx] && (
<img src={tileData[currentIdx].img} alt={tileData[currentIdx].img} />
)}
</div>
);
return (
<div>
<GridList cols={2.5}>
{tileData.map((tile, idx) => (
<img
src={tile.img}
alt={tile.img}
onClick={() => handleOpen(idx)}
key={tile.img}
/>
))}
</GridList>
<Modal open={open} onClose={handleClose}>
{body}
</Modal>
</div>
);
}
Live Demo

How to pass className style to sub component in `material-ui`?

Material UI uses className for stying. But how can I pass the style to sub react component?
Below is my style definition.
const styles = createStyles({
root: {
backgroundColor: 'transparent !important',
boxShadow: 'none',
paddingTop: '25px',
color: '#FFFFFF'
},
subComponentStyle: {
...
}
});
And I use this like:
...
const NavigationBar = (props) => {
const { classes } = props;
return (
<div className={classes.root}>
// Add other code here
<SubComponent ... > // how to pass `classes.subComponentStyle` style here
</div>
)
}
...
export default withStyles(styles)(NavigationBar);
If the SubComponent component is also exported with withStyles. How can I pass some styles to override its own styling?
My SubComponent is exported as:
const styles = createStyles({
...
});
const SubComponent = ({classes}) => {
...
}
export default withStyles(styles)(SubComponent);
as you can see, it has its own classes. I don't want to override its classes completely. Is there a way to merge the passed in classes with its internal classes?
// Edited to merged styles
MUI will merge styles if you pass the classes as well as wrap the child withStyles. ie:
import { styles } from './NavStyles'
const NavigationBar = (props) => {
const { classes } = props;
return (
<div className={classes.root}>
<SubComponent classes={classes} >
</div>
)
};
export default withStyles(styles)(NavigationBar);
and in then also apply styles to the child component
import { styles } from './SubCompStyles'
const SubComponent = ({classes}) => {
// classes object is a merge of both parent and child styles
// ... component logic
};
export default withStyles(styles)(SubComponent)
Heres how you can do it with hook API:
Sub component
const useStyles = makeStyles((theme) => ({
root: {
borderRadius: 3,
color: 'white',
padding: '0 30px',
width: '12em',
height: 43,
borderRadius: 21.5,
textTransform: 'capitalize',
... your styles here.
},
}))
export default function AuthSecondaryButton(props) {
const classes = useStyles()
console.log('s', props.className)
return (
<Button
{...props}
className={clsx({
[classes.root]: true,
[props.className]: true,
})}
/>
)
}
Parent component
const useStyles = makeStyles((theme) => ({
secondaryButton: {
marginTop: theme.spacing(1),
},
}))
export default function App(props) {
const classes = useStyles()
return(
<AuthSecondaryButton
onClick={onClickSecondaryButton}
className={classes.secondaryButton}
>
Sign Up
</AuthSecondaryButton>
)
A slight tweak to #clever_usernames approach.
This uses classnames package instead of the clsx package, which we use in our project.
Replacing this...
className={clsx({
[classes.root]: true,
[props.className]: true,
})}
with this...
className={classNames(classes.root, props.className)}
Full Example
Sub component
import classNames from 'classnames'
const useStyles = makeStyles((theme) => ({
root: {
borderRadius: 3,
color: 'white',
padding: '0 30px',
width: '12em',
height: 43,
borderRadius: 21.5,
textTransform: 'capitalize',
... your styles here.
},
}))
export default function AuthSecondaryButton(props) {
const classes = useStyles()
console.log('s', props.className)
return (
<Button
{...props}
className={classNames(classes.root, props.className)}
/>
)
}
Parent component
const useStyles = makeStyles((theme) => ({
secondaryButton: {
marginTop: theme.spacing(1),
},
}))
export default function App(props) {
const classes = useStyles()
return(
<AuthSecondaryButton
onClick={onClickSecondaryButton}
className={classes.secondaryButton}
>
Sign Up
</AuthSecondaryButton>
)

Resources