Popper
A Popper can be used to display some content on top of another. It’s an alternative to react-popper.
Some important features of the Popper component:
- 🕷 Popper relies on the 3rd party library (Popper.js) for perfect positioning.
- 💄 It’s an alternative API to react-popper. It aims for simplicity.
- Its child element is a Portal on the body
of the document to avoid rendering problems. You can disable this
behavior with
disablePortal. - The scroll isn’t blocked like with the Popover component. The placement of the popper updates with the available area in the viewport.
- Clicking away does not hide the Popper component. If you need this behavior, you can use the Click-Away Listener.
- The
anchorElis passed as the reference object to create a newPopper.jsinstance.
How the live demos on this page work. The open state
is kept in the URL with reactRouter: the
route #/simple-popper renders the popper, any other URL
does not. The toggle button is part of each route and links to the other
state. anchorEl accepts a function, so the anchor is looked
up by id with JS().
Basic Popper
anchorById <- function(id) JS(sprintf("() => document.getElementById('%s')", id))
simplePopper <- function(open) {
tagList(
Button(
id = "simple-popper-button",
variant = "outlined",
`aria-describedby` = "simple-popper",
href = if (open) "#/" else "#/simple-popper",
"Toggle Popper"
),
if (open) Popper(
id = "simple-popper",
open = TRUE,
anchorEl = anchorById("simple-popper-button"),
Box(sx = list(border = 1, p = 1, bgcolor = "background.paper"), "The content of the Popper.")
)
)
}
muiMaterialPage(
CssBaseline(),
RouterProvider(
router = createHashRouter(
Route(path = "simple-popper", element = simplePopper(TRUE)),
Route(path = "*", element = simplePopper(FALSE))
)
)
)JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Popper from '@mui/material/Popper';
export default function SimplePopper() {
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(anchorEl ? null : event.currentTarget);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popper' : undefined;
return (
<div>
<button aria-describedby={id} type="button" onClick={handleClick}>
Toggle Popper
</button>
<Popper id={id} open={open} anchorEl={anchorEl}>
<Box sx={{ border: 1, p: 1, bgcolor: 'background.paper' }}>
The content of the Popper.
</Box>
</Popper>
</div>
);
}Transitions
The open/close state of the popper can be animated with a render prop child and a transition component. This component should respect the following conditions:
- Be a direct child descendent of the popper.
- Call the
onEntercallback prop when the enter transition starts. - Call the
onExitedcallback prop when the exit transition is completed. These two callbacks allow the popper to unmount the child content when closed and fully transitioned.
Popper has built-in support for react-transition-group.
A render prop (a function child) cannot be written in R. Wrapping the
content in a transition such as Fade(in = TRUE) animates
the opening, as the content mounts when the route becomes active:
transitionsPopper <- function(open) {
tagList(
Button(
id = "transitions-popper-button",
variant = "outlined",
href = if (open) "#/" else "#/transitions-popper",
"Toggle Popper"
),
if (open) Popper(
open = TRUE,
anchorEl = anchorById("transitions-popper-button"),
Fade(
`in` = TRUE,
timeout = 350,
Box(sx = list(border = 1, p = 1, bgcolor = "background.paper"), "The content of the Popper.")
)
)
)
}
muiMaterialPage(
CssBaseline(),
RouterProvider(
router = createHashRouter(
Route(path = "transitions-popper", element = transitionsPopper(TRUE)),
Route(path = "*", element = transitionsPopper(FALSE))
)
)
)JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Popper from '@mui/material/Popper';
import Fade from '@mui/material/Fade';
export default function TransitionsPopper() {
const [open, setOpen] = React.useState(false);
const [anchorEl, setAnchorEl] = React.useState(null);
const handleClick = (event) => {
setAnchorEl(event.currentTarget);
setOpen((previousOpen) => !previousOpen);
};
const canBeOpen = open && Boolean(anchorEl);
const id = canBeOpen ? 'transition-popper' : undefined;
return (
<div>
<button aria-describedby={id} type="button" onClick={handleClick}>
Toggle Popper
</button>
<Popper id={id} open={open} anchorEl={anchorEl} transition>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Box sx={{ border: 1, p: 1, bgcolor: 'background.paper' }}>
The content of the Popper.
</Box>
</Fade>
)}
</Popper>
</div>
);
}Positioned popper
The placement prop sets where the popper appears
relative to its anchor. Here each placement is a route
(#/positioned-popper/<placement>). The buttons link
to their own placement, or back to #/ when their popper is
open.
placements <- list(
top = c("top-start", "top", "top-end"),
left = c("left-start", "left", "left-end"),
right = c("right-start", "right", "right-end"),
bottom = c("bottom-start", "bottom", "bottom-end")
)
positionedPopper <- function(active = NULL) {
placementButton <- function(placement) {
Button(
id = paste0("placement-", placement),
href = if (identical(placement, active)) "#/" else paste0("#/positioned-popper/", placement),
placement
)
}
Box(
sx = list(width = 500, maxWidth = "100%"),
if (!is.null(active)) Popper(
sx = list(zIndex = 1200),
open = TRUE,
anchorEl = anchorById(paste0("placement-", active)),
placement = active,
Fade(`in` = TRUE, timeout = 350, Paper(Typography(sx = list(p = 2), "The content of the Popper.")))
),
Stack(direction = "row", sx = list(justifyContent = "center"), lapply(placements$top, placementButton)),
Box(
sx = list(display = "flex", justifyContent = "space-between"),
Stack(direction = "column", sx = list(alignItems = "flex-start"), lapply(placements$left, placementButton)),
Stack(direction = "column", sx = list(alignItems = "flex-end"), lapply(placements$right, placementButton))
),
Stack(direction = "row", sx = list(justifyContent = "center"), lapply(placements$bottom, placementButton))
)
}
muiMaterialPage(
CssBaseline(),
RouterProvider(
router = createHashRouter(
lapply(unlist(placements, use.names = FALSE), function(placement) {
Route(path = paste0("positioned-popper/", placement), element = positionedPopper(placement))
}),
Route(path = "*", element = positionedPopper())
)
)
)JS code
import * as React from 'react';
import Box from '@mui/material/Box';
import Popper from '@mui/material/Popper';
import Typography from '@mui/material/Typography';
import Stack from '@mui/material/Stack';
import Button from '@mui/material/Button';
import Fade from '@mui/material/Fade';
import Paper from '@mui/material/Paper';
export default function PositionedPopper() {
const [anchorEl, setAnchorEl] = React.useState(null);
const [open, setOpen] = React.useState(false);
const [placement, setPlacement] = React.useState();
const handleClick = (newPlacement) => (event) => {
setAnchorEl(event.currentTarget);
setOpen((prev) => placement !== newPlacement || !prev);
setPlacement(newPlacement);
};
return (
<Box sx={{ width: 500 }}>
<Popper
// Note: The following zIndex style is specifically for documentation purposes and may not be necessary in your application.
sx={{ zIndex: 1200 }}
open={open}
anchorEl={anchorEl}
placement={placement}
transition
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={350}>
<Paper>
<Typography sx={{ p: 2 }}>The content of the Popper.</Typography>
</Paper>
</Fade>
)}
</Popper>
<Stack direction="row" sx={{ justifyContent: 'center' }}>
<Button onClick={handleClick('top-start')}>top-start</Button>
<Button onClick={handleClick('top')}>top</Button>
<Button onClick={handleClick('top-end')}>top-end</Button>
</Stack>
<Box sx={{ display: 'flex', justifyContent: 'space-between' }}>
<Stack direction="column" sx={{ alignItems: 'flex-start' }}>
<Button onClick={handleClick('left-start')}>left-start</Button>
<Button onClick={handleClick('left')}>left</Button>
<Button onClick={handleClick('left-end')}>left-end</Button>
</Stack>
<Stack direction="column" sx={{ alignItems: 'flex-end' }}>
<Button onClick={handleClick('right-start')}>right-start</Button>
<Button onClick={handleClick('right')}>right</Button>
<Button onClick={handleClick('right-end')}>right-end</Button>
</Stack>
</Box>
<Stack direction="row" sx={{ justifyContent: 'center' }}>
<Button onClick={handleClick('bottom-start')}>bottom-start</Button>
<Button onClick={handleClick('bottom')}>bottom</Button>
<Button onClick={handleClick('bottom-end')}>bottom-end</Button>
</Stack>
</Box>
);
}Scroll playground
The MUI documentation has an interactive playground for the
flip and preventOverflow modifiers of
Popper.js. The modifiers are passed with the modifiers
prop, for example:
Virtual element
In React, the value of the anchorEl prop can be a fake
DOM element shaped like a Popper.js VirtualElement,
for example to show a popper next to selected text. This needs custom
JavaScript and has no R equivalent.
Popover or Popper?
For most “open on click” cases, Popover is
simpler: Popover.triggerId() manages the open state and
closes on click-away. Use Popper() when the page must stay
scrollable and interactive while the content is shown.
