Building Custom Positioned Elements with React MUI Popper
When developing React applications, precisely positioning elements like tooltips, dropdowns, and custom menus can be challenging. The Material UI (MUI) Popper component provides a powerful solution for creating floating elements with pixel-perfect positioning. In this article, I'll walk you through how to leverage MUI's Popper component to build custom positioned elements that enhance your application's user experience.
Understanding MUI Popper and What We'll Build
The MUI Popper component is a wrapper around the Popper.js library, which provides precise positioning capabilities for floating elements. Unlike simpler positioning solutions, Popper handles complex scenarios including:
- Automatic repositioning when elements would overflow the viewport
- Maintaining position during scrolling and resizing
- Supporting multiple placement options and alignment strategies
- Handling complex positioning calculations efficiently
By the end of this guide, you'll understand how to:
- Implement basic Popper functionality with various placement options
- Create custom positioned components like tooltips and dropdown menus
- Handle edge cases with modifiers and transition effects
- Apply advanced positioning strategies for complex UI requirements
- Optimize Popper performance in real-world applications
MUI Popper Deep Dive
Before we start building, let's thoroughly understand the Popper component and its capabilities.
Component Overview
The Popper component is part of MUI's core library and provides positioning functionality without imposing any specific UI or styling. This makes it extremely flexible for building custom positioned elements. It uses the Popper.js v2 library underneath, which handles all the complex positioning calculations.
Key Props and Configuration
The Popper component accepts numerous props that control its behavior and appearance. Here are the most important ones:
Prop | Type | Default | Description |
---|---|---|---|
anchorEl | Element | Object | Function | null | The reference element used to position the Popper |
children | Node | Function | - | The content of the Popper |
open | Boolean | false | Controls the visibility of the Popper |
placement | String | 'bottom' | Position where the Popper should be displayed |
transition | Boolean | false | If true, adds transition when the Popper mounts/unmounts |
disablePortal | Boolean | false | Disables using Portal to render children into a new subtree |
modifiers | Array | [] | Popper.js modifiers to customize behavior |
popperOptions | Object | Pass options directly to Popper.js instance | |
popperRef | Ref | - | Ref to get access to the popper instance |
keepMounted | Boolean | false | Always keep children in the DOM |
Placement Options
The placement
prop is particularly important as it determines where your popper will be positioned relative to the anchor element. MUI Popper supports 12 different placement options:
top
- Element above the anchortop-start
- Element above, aligned with the left edgetop-end
- Element above, aligned with the right edgebottom
- Element below the anchorbottom-start
- Element below, aligned with the left edgebottom-end
- Element below, aligned with the right edgeright
- Element to the right of the anchorright-start
- Element to the right, aligned with the top edgeright-end
- Element to the right, aligned with the bottom edgeleft
- Element to the left of the anchorleft-start
- Element to the left, aligned with the top edgeleft-end
- Element to the left, aligned with the bottom edge
Controlled vs Uncontrolled Usage
The Popper component is typically used in a controlled manner, where the open
state is managed externally. This gives you complete control over when the Popper is shown or hidden.
const [open, setOpen] = React.useState(false);
const anchorRef = React.useRef(null);
return (
<>
<Button ref={anchorRef} onClick={() => setOpen(!open)}>
Toggle Popper
</Button>
<Popper open={open} anchorEl={anchorRef.current}>
<Paper>Popper content</Paper>
</Popper>
</>
);
Portals and Rendering
By default, the Popper component uses React's Portal feature to render the popper content at the end of the document body. This prevents CSS overflow, z-index, or positioning context issues that might interfere with the popper's visibility. You can disable this behavior with the disablePortal
prop if needed.
Modifiers System
One of the most powerful features of Popper is its modifiers system. Modifiers are plugins that can change the behavior of the positioning algorithm. For example:
flip
- Automatically flips the popper's placement when it starts to overlap the reference elementpreventOverflow
- Prevents the popper from being positioned outside the boundaryoffset
- Offsets the popper from its reference element
Accessibility Considerations
When using Popper, you need to ensure that your custom UI components remain accessible:
- Use appropriate ARIA attributes (
aria-expanded
,aria-haspopup
, etc.) - Ensure keyboard navigation works correctly
- Add proper focus management
- Include appropriate role attributes
Setting Up Your Project
Let's start by setting up a React project with MUI installed. If you already have a project, you can skip to the next section.
Installing Dependencies
First, create a new React project and install the necessary dependencies:
npx create-react-app mui-popper-demo
cd mui-popper-demo
npm install @mui/material @mui/icons-material @emotion/react @emotion/styled
This installs React, MUI core components, MUI icons, and the required Emotion styling dependencies.
Basic Project Structure
Let's create a simple project structure to organize our Popper examples:
src/
├── components/
│ ├── BasicPopper.jsx
│ ├── CustomTooltip.jsx
│ ├── PositionedMenu.jsx
│ └── AdvancedPopper.jsx
├── App.js
├── index.js
└── ...
Creating a Basic Popper Component
Let's start with a simple implementation to understand the core functionality of the Popper component.
Step 1: Create a Basic Popper with Click Trigger
First, let's create a basic popper that appears when a button is clicked:
import React, { useState, useRef } from 'react';
import { Box, Button, Popper, Paper, Typography } from '@mui/material';
function BasicPopper() {
// State to control popper visibility
const [open, setOpen] = useState(false);
// Reference to the anchor element
const anchorRef = useRef(null);
// Toggle popper visibility
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Popper
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
>
<Paper sx={{ p: 2, mt: 1, width: 200 }}>
<Typography>This is a basic popper component.</Typography>
</Paper>
</Popper>
</Box>
);
}
export default BasicPopper;
In this example:
- We create a state variable
open
to control the visibility of the popper - We use
useRef
to create a reference to the button that will trigger the popper - The
Popper
component uses theanchorRef.current
as its anchor element - We set the
placement
to "bottom" to position the popper below the button - The popper content is wrapped in a
Paper
component for styling
Step 2: Exploring Different Placement Options
Now, let's enhance our basic popper to demonstrate different placement options:
import React, { useState, useRef } from 'react';
import {
Box,
Button,
Popper,
Paper,
Typography,
Grid,
FormControl,
InputLabel,
Select,
MenuItem
} from '@mui/material';
function PlacementPopper() {
const [open, setOpen] = useState(false);
const [placement, setPlacement] = useState('bottom');
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handlePlacementChange = (event) => {
setPlacement(event.target.value);
};
// All possible placement options
const placements = [
'top-start', 'top', 'top-end',
'left-start', 'left', 'left-end',
'right-start', 'right', 'right-end',
'bottom-start', 'bottom', 'bottom-end',
];
return (
<Box sx={{ m: 2 }}>
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<FormControl fullWidth>
<InputLabel>Placement</InputLabel>
<Select
value={placement}
label="Placement"
onChange={handlePlacementChange}
>
{placements.map((p) => (
<MenuItem key={p} value={p}>{p}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<Box display="flex" justifyContent="center" my={4}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Popper ({placement})
</Button>
</Box>
<Popper
open={open}
anchorEl={anchorRef.current}
placement={placement}
>
<Paper sx={{ p: 2, maxWidth: 300 }}>
<Typography>
This popper is positioned at the <b>{placement}</b> of the button.
</Typography>
</Paper>
</Popper>
</Grid>
</Grid>
</Box>
);
}
export default PlacementPopper;
In this enhanced example:
- We added a dropdown to select from the 12 different placement options
- The selected placement is passed to the Popper component
- The UI displays the current placement for clarity
- We centered the button to better demonstrate the different placements
Step 3: Adding Transitions for a Smoother Experience
Let's improve our popper by adding transition effects when it appears and disappears:
import React, { useState, useRef } from 'react';
import {
Box,
Button,
Popper,
Paper,
Typography,
Fade,
Grow
} from '@mui/material';
import { ToggleButton, ToggleButtonGroup } from '@mui/material';
function TransitionPopper() {
const [open, setOpen] = useState(false);
const [transition, setTransition] = useState('fade');
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleTransitionChange = (event, newTransition) => {
if (newTransition !== null) {
setTransition(newTransition);
}
};
return (
<Box sx={{ m: 2 }}>
<Box mb={2}>
<ToggleButtonGroup
value={transition}
exclusive
onChange={handleTransitionChange}
aria-label="transition selection"
>
<ToggleButton value="fade" aria-label="fade transition">
Fade
</ToggleButton>
<ToggleButton value="grow" aria-label="grow transition">
Grow
</ToggleButton>
<ToggleButton value="none" aria-label="no transition">
None
</ToggleButton>
</ToggleButtonGroup>
</Box>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Popper with {transition} transition
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
transition
>
{({ TransitionProps }) => {
// Apply different transition based on selection
if (transition === 'fade') {
return (
<Fade {...TransitionProps} timeout={350}>
<Paper sx={{ p: 2, mt: 1, width: 250 }}>
<Typography>
This popper uses a Fade transition effect.
</Typography>
</Paper>
</Fade>
);
} else if (transition === 'grow') {
return (
<Grow {...TransitionProps} timeout={350}>
<Paper sx={{ p: 2, mt: 1, width: 250 }}>
<Typography>
This popper uses a Grow transition effect.
</Typography>
</Paper>
</Grow>
);
} else {
return (
<Paper sx={{ p: 2, mt: 1, width: 250 }}>
<Typography>
This popper has no transition effect.
</Typography>
</Paper>
);
}
}}
</Popper>
</Box>
);
}
export default TransitionPopper;
This example demonstrates:
- Using the
transition
prop on the Popper component - Applying MUI transition components (
Fade
andGrow
) to the popper content - Toggling between different transition effects
- Handling the transition props correctly
The Popper component provides a render prop with TransitionProps
that we pass to our transition component. This ensures the transition is properly coordinated with the popper's visibility.
Building Practical Components with Popper
Now that we understand the basics, let's build some practical UI components using the Popper.
Creating a Custom Tooltip Component
The built-in MUI Tooltip is great, but sometimes you need more customization. Let's build our own tooltip using Popper:
import React, { useState } from 'react';
import {
Box,
Popper,
Paper,
Typography,
Fade,
styled
} from '@mui/material';
// Styled component for the tooltip container
const TooltipContent = styled(Paper)(({ theme }) => ({
padding: theme.spacing(1.5),
backgroundColor: theme.palette.grey[900],
color: theme.palette.common.white,
maxWidth: 300,
fontSize: theme.typography.pxToRem(12),
borderRadius: theme.shape.borderRadius,
}));
// The custom tooltip component
function CustomTooltip({ children, title, placement = 'top', arrow = true }) {
const [anchorEl, setAnchorEl] = useState(null);
const [open, setOpen] = useState(false);
// Show tooltip on mouse enter
const handleMouseEnter = (event) => {
setAnchorEl(event.currentTarget);
setOpen(true);
};
// Hide tooltip on mouse leave
const handleMouseLeave = () => {
setOpen(false);
};
return (
<Box
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
sx={{ display: 'inline-block' }}
>
{children}
<Popper
open={open}
anchorEl={anchorEl}
placement={placement}
transition
modifiers={[
{
name: 'offset',
options: {
offset: [0, arrow ? 10 : 8],
},
},
]}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={200}>
<TooltipContent>
{arrow && (
<Box
sx={{
position: 'absolute',
width: 10,
height: 10,
bgcolor: 'grey.900',
transform: 'rotate(45deg)',
...(placement.includes('top') && {
bottom: -5,
left: 'calc(50% - 5px)',
}),
...(placement.includes('bottom') && {
top: -5,
left: 'calc(50% - 5px)',
}),
...(placement.includes('left') && {
right: -5,
top: 'calc(50% - 5px)',
}),
...(placement.includes('right') && {
left: -5,
top: 'calc(50% - 5px)',
}),
}}
/>
)}
<Typography variant="body2">{title}</Typography>
</TooltipContent>
</Fade>
)}
</Popper>
</Box>
);
}
// Usage example component
function CustomTooltipDemo() {
return (
<Box sx={{ m: 2 }}>
<Typography paragraph>
Hover over these elements to see custom tooltips:
</Typography>
<Box display="flex" gap={3} flexWrap="wrap">
<CustomTooltip title="This is a top tooltip">
<Typography
variant="button"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
>
Top Tooltip
</Typography>
</CustomTooltip>
<CustomTooltip title="This is a bottom tooltip" placement="bottom">
<Typography
variant="button"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
>
Bottom Tooltip
</Typography>
</CustomTooltip>
<CustomTooltip title="This is a left tooltip" placement="left">
<Typography
variant="button"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
>
Left Tooltip
</Typography>
</CustomTooltip>
<CustomTooltip title="This is a right tooltip" placement="right">
<Typography
variant="button"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
>
Right Tooltip
</Typography>
</CustomTooltip>
<CustomTooltip
title="This tooltip has no arrow"
placement="top"
arrow={false}
>
<Typography
variant="button"
sx={{ cursor: 'pointer', textDecoration: 'underline' }}
>
No Arrow
</Typography>
</CustomTooltip>
</Box>
</Box>
);
}
export default CustomTooltipDemo;
This custom tooltip implementation:
- Uses mouse events to trigger the tooltip display
- Applies a fade transition for smooth appearance
- Includes an optional arrow that points to the anchor element
- Supports all placement options
- Uses styled components for consistent styling
- Implements proper offset using Popper modifiers
Building a Dropdown Menu Component
Next, let's create a dropdown menu component using Popper:
import React, { useState, useRef } from 'react';
import {
Box,
Button,
Popper,
Paper,
MenuList,
MenuItem,
ClickAwayListener,
Grow,
Divider,
Typography
} from '@mui/material';
import {
ArrowDropDown as ArrowDropDownIcon,
ContentCopy as ContentCopyIcon,
Delete as DeleteIcon,
Edit as EditIcon,
Archive as ArchiveIcon
} from '@mui/icons-material';
function PopperMenu() {
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const handleMenuItemClick = (action) => (event) => {
console.log(`Action selected: ${action}`);
setOpen(false);
};
// Handle keyboard navigation
const handleKeyDown = (event) => {
if (event.key === 'Escape') {
setOpen(false);
}
};
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
endIcon={<ArrowDropDownIcon />}
aria-controls={open ? 'menu-list' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
Actions Menu
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom-start"
transition
disablePortal
role={undefined}
sx={{ zIndex: 1000 }}
>
{({ TransitionProps }) => (
<Grow
{...TransitionProps}
style={{ transformOrigin: 'top left' }}
>
<Paper elevation={3} sx={{ mt: 1, width: 200 }}>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
autoFocusItem={open}
id="menu-list"
onKeyDown={handleKeyDown}
aria-labelledby="menu-button"
>
<MenuItem onClick={handleMenuItemClick('edit')}>
<EditIcon fontSize="small" sx={{ mr: 1 }} />
Edit
</MenuItem>
<MenuItem onClick={handleMenuItemClick('copy')}>
<ContentCopyIcon fontSize="small" sx={{ mr: 1 }} />
Duplicate
</MenuItem>
<Divider />
<MenuItem onClick={handleMenuItemClick('archive')}>
<ArchiveIcon fontSize="small" sx={{ mr: 1 }} />
Archive
</MenuItem>
<MenuItem
onClick={handleMenuItemClick('delete')}
sx={{ color: 'error.main' }}
>
<DeleteIcon fontSize="small" sx={{ mr: 1 }} />
Delete
</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</Box>
);
}
// Usage example with multiple menus
function PopperMenuDemo() {
return (
<Box>
<Typography variant="h6" sx={{ mb: 2 }}>
Dropdown Menu with Popper
</Typography>
<Box display="flex" gap={2}>
<PopperMenu />
</Box>
</Box>
);
}
export default PopperMenuDemo;
This dropdown menu implementation:
- Uses a button with an arrow icon as the trigger
- Shows a menu with icons and a divider when clicked
- Closes when clicking outside using
ClickAwayListener
- Handles keyboard navigation (Escape key to close)
- Uses a Grow transition for a natural appearance
- Includes proper ARIA attributes for accessibility
- Has a consistent visual style with MUI's design language
Advanced Popper Techniques
Now let's explore some advanced techniques for working with the Popper component.
Virtual Element Positioning
Sometimes you need to position a popper relative to a point that isn't tied to a DOM element. Popper.js supports "virtual elements" for this purpose:
import React, { useState, useEffect } from 'react';
import {
Box,
Paper,
Typography,
Popper,
Button
} from '@mui/material';
function VirtualElementPopper() {
const [open, setOpen] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [virtualElement, setVirtualElement] = useState(null);
// Create a virtual element based on the current position
useEffect(() => {
if (position.x && position.y) {
setVirtualElement({
getBoundingClientRect: () => ({
top: position.y,
left: position.x,
bottom: position.y,
right: position.x,
width: 0,
height: 0,
}),
});
}
}, [position]);
// Handle click on the container to update position
const handleBoxClick = (event) => {
// Get click coordinates relative to the viewport
const x = event.clientX;
const y = event.clientY;
setPosition({ x, y });
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<Box sx={{ m: 2 }}>
<Typography paragraph>
Click anywhere in the box below to show a popper at that position:
</Typography>
<Box
onClick={handleBoxClick}
sx={{
height: 300,
bgcolor: 'action.hover',
border: '1px dashed',
borderColor: 'primary.main',
borderRadius: 1,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
}}
>
<Typography color="text.secondary">
Click anywhere in this area
</Typography>
</Box>
{virtualElement && (
<Popper
open={open}
anchorEl={virtualElement}
placement="top"
>
<Paper
sx={{
p: 2,
maxWidth: 200,
bgcolor: 'info.light',
color: 'info.contrastText'
}}
>
<Typography variant="body2">
Popper positioned at X: {Math.round(position.x)}, Y: {Math.round(position.y)}
</Typography>
<Button
size="small"
onClick={handleClose}
sx={{ mt: 1 }}
>
Close
</Button>
</Paper>
</Popper>
)}
</Box>
);
}
export default VirtualElementPopper;
This example demonstrates:
- Creating a virtual element with the
getBoundingClientRect
method - Positioning a popper at arbitrary coordinates on the screen
- Updating the virtual element when the position changes
- Handling user interactions to trigger the popper
This technique is useful for context menus, tooltips that follow the cursor, or any UI element that needs to be positioned relative to a point rather than a DOM element.
Using Popper Modifiers for Advanced Positioning
Popper.js provides modifiers that can customize the positioning behavior. Let's implement some advanced positioning with custom modifiers:
import React, { useState, useRef } from 'react';
import {
Box,
Button,
Popper,
Paper,
Typography,
Slider,
Grid,
FormControlLabel,
Switch
} from '@mui/material';
function ModifierPopper() {
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
// Modifier configuration
const [offset, setOffset] = useState(10);
const [preventOverflow, setPreventOverflow] = useState(true);
const [flip, setFlip] = useState(true);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
// Create modifiers array based on current settings
const modifiers = [
{
name: 'offset',
options: {
offset: [0, offset],
},
},
{
name: 'preventOverflow',
enabled: preventOverflow,
options: {
boundary: document.body,
},
},
{
name: 'flip',
enabled: flip,
options: {
fallbackPlacements: ['top', 'right', 'left'],
},
},
];
return (
<Box sx={{ m: 2 }}>
<Typography variant="h6" gutterBottom>
Popper Modifiers Demo
</Typography>
<Grid container spacing={2} sx={{ mb: 3 }}>
<Grid item xs={12} sm={6}>
<Typography id="offset-slider" gutterBottom>
Offset: {offset}px
</Typography>
<Slider
value={offset}
onChange={(e, newValue) => setOffset(newValue)}
aria-labelledby="offset-slider"
min={0}
max={50}
marks={[
{ value: 0, label: '0px' },
{ value: 25, label: '25px' },
{ value: 50, label: '50px' },
]}
/>
</Grid>
<Grid item xs={12} sm={6}>
<FormControlLabel
control={
<Switch
checked={preventOverflow}
onChange={(e) => setPreventOverflow(e.target.checked)}
/>
}
label="Prevent Overflow"
/>
<Box mt={1}>
<FormControlLabel
control={
<Switch
checked={flip}
onChange={(e) => setFlip(e.target.checked)}
/>
}
label="Auto Flip"
/>
</Box>
</Grid>
</Grid>
<Box display="flex" justifyContent="center" sx={{ position: 'relative' }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Popper
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
modifiers={modifiers}
>
<Paper sx={{ p: 2, width: 250 }}>
<Typography paragraph>
This popper uses custom modifiers:
</Typography>
<Typography variant="body2" component="ul" sx={{ pl: 2 }}>
<li>Offset: {offset}px</li>
<li>Prevent Overflow: {preventOverflow ? 'Enabled' : 'Disabled'}</li>
<li>Auto Flip: {flip ? 'Enabled' : 'Disabled'}</li>
</Typography>
<Typography variant="body2" sx={{ mt: 1 }}>
Try resizing the window or scrolling to see how these settings affect the popper's position.
</Typography>
</Paper>
</Popper>
</Box>
</Box>
);
}
export default ModifierPopper;
This example demonstrates:
- Using the
offset
modifier to create space between the anchor and popper - Enabling/disabling the
preventOverflow
modifier to control boundary detection - Toggling the
flip
modifier to control automatic placement adjustment - Interactive controls to adjust modifier settings in real-time
- Explaining how each modifier affects the popper's behavior
Creating an Interactive Popover with Popper
Let's build a more complex popover component that can be used for forms or interactive content:
import React, { useState, useRef } from 'react';
import {
Box,
Button,
Popper,
Paper,
Typography,
TextField,
IconButton,
ClickAwayListener,
Fade,
Stack
} from '@mui/material';
import {
Close as CloseIcon,
Save as SaveIcon
} from '@mui/icons-material';
function InteractivePopover() {
const [open, setOpen] = useState(false);
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleClose = (event) => {
if (anchorRef.current && anchorRef.current.contains(event.target)) {
return;
}
setOpen(false);
};
const handleSubmit = (event) => {
event.preventDefault();
console.log('Form submitted:', { name, email });
setOpen(false);
// In a real app, you would send this data to your backend
};
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
aria-controls={open ? 'interactive-popover' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
>
Open Form Popover
</Button>
<Popper
id="interactive-popover"
open={open}
anchorEl={anchorRef.current}
placement="bottom-start"
transition
modifiers={[
{
name: 'offset',
options: {
offset: [0, 10],
},
},
]}
>
{({ TransitionProps }) => (
<Fade {...TransitionProps} timeout={250}>
<Paper elevation={4}>
<ClickAwayListener onClickAway={handleClose}>
<Box component="form" onSubmit={handleSubmit} sx={{ p: 2, width: 300 }}>
<Box display="flex" justifyContent="space-between" alignItems="center" mb={2}>
<Typography variant="h6">Contact Form</Typography>
<IconButton size="small" onClick={handleClose} aria-label="close">
<CloseIcon fontSize="small" />
</IconButton>
</Box>
<Stack spacing={2}>
<TextField
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
fullWidth
size="small"
required
/>
<TextField
label="Email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
fullWidth
size="small"
required
/>
<Box display="flex" justifyContent="flex-end">
<Button
type="submit"
variant="contained"
startIcon={<SaveIcon />}
>
Submit
</Button>
</Box>
</Stack>
</Box>
</ClickAwayListener>
</Paper>
</Fade>
)}
</Popper>
</Box>
);
}
export default InteractivePopover;
This interactive popover example:
- Contains a form with input fields and a submit button
- Handles form submission and validation
- Uses
ClickAwayListener
to close when clicking outside - Includes a close button in the header
- Applies proper styling and spacing for a good UX
- Implements proper ARIA attributes for accessibility
Optimizing Popper Performance
When working with Popper in larger applications, performance can become a concern. Here are some techniques to optimize Popper usage:
Conditional Rendering and Lazy Loading
import React, { useState, useRef, lazy, Suspense } from 'react';
import {
Box,
Button,
CircularProgress
} from '@mui/material';
// Lazy load the popper content
const LazyPopperContent = lazy(() =>
import('./LazyPopperContent').then(module => ({
default: module.default
}))
);
function OptimizedPopper() {
const [open, setOpen] = useState(false);
const [contentLoaded, setContentLoaded] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => {
if (!prevOpen) {
// When opening, mark that we've loaded the content
setContentLoaded(true);
}
return !prevOpen;
});
};
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
{open ? 'Close Popper' : 'Open Popper'}
</Button>
{/* Only render the Popper when needed */}
{(open || contentLoaded) && (
<Suspense fallback={<CircularProgress size={24} />}>
<LazyPopperContent
open={open}
anchorEl={anchorRef.current}
/>
</Suspense>
)}
</Box>
);
}
export default OptimizedPopper;
// In a separate file: LazyPopperContent.jsx
import React from 'react';
import { Popper, Paper, Typography } from '@mui/material';
function LazyPopperContent({ open, anchorEl }) {
return (
<Popper
open={open}
anchorEl={anchorEl}
placement="bottom"
>
<Paper sx={{ p: 2, width: 300 }}>
<Typography>
This content was lazy-loaded for better performance.
</Typography>
</Paper>
</Popper>
);
}
export default LazyPopperContent;
This optimization technique:
- Uses React's lazy loading to only load the popper content when needed
- Conditionally renders the popper component based on its visibility
- Shows a loading indicator while the content is being loaded
- Maintains the loaded content in the DOM for faster subsequent opens
Debouncing Position Updates
When a popper needs to update its position frequently (e.g., during scrolling), debouncing can improve performance:
import React, { useState, useRef, useEffect, useCallback } from 'react';
import {
Box,
Button,
Popper,
Paper,
Typography
} from '@mui/material';
// Debounce helper function
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
function DebouncedPopper() {
const [open, setOpen] = useState(false);
const [popperInstance, setPopperInstance] = useState(null);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
// Create a debounced update function
const debouncedUpdate = useCallback(
debounce(() => {
if (popperInstance) {
popperInstance.update();
}
}, 50),
[popperInstance]
);
// Set up scroll and resize listeners
useEffect(() => {
if (open) {
window.addEventListener('scroll', debouncedUpdate);
window.addEventListener('resize', debouncedUpdate);
}
return () => {
window.removeEventListener('scroll', debouncedUpdate);
window.removeEventListener('resize', debouncedUpdate);
};
}, [open, debouncedUpdate]);
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Debounced Popper
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
popperRef={(instance) => {
setPopperInstance(instance);
}}
>
<Paper sx={{ p: 2, width: 300 }}>
<Typography paragraph>
This popper uses debounced position updates for better performance.
</Typography>
<Typography variant="body2">
Try scrolling or resizing the window. The popper's position will
update efficiently without causing performance issues.
</Typography>
</Paper>
</Popper>
</Box>
);
}
export default DebouncedPopper;
This debouncing technique:
- Creates a debounced update function that limits how often the popper recalculates its position
- Uses the
popperRef
prop to get access to the popper instance - Manually calls the popper's update method after debouncing
- Attaches and detaches event listeners appropriately
- Improves performance during scrolling and resizing
Best Practices and Common Issues
When working with MUI's Popper component, there are several best practices to follow and common issues to be aware of.
Best Practices for Using Popper
-
Always use a controlled pattern: Manage the
open
state externally for better control and predictability. -
Handle focus management: When using Popper for interactive elements like menus, ensure proper focus management for accessibility.
-
Use ClickAwayListener: For interactive poppers, always include a ClickAwayListener to close the popper when clicking outside.
-
Apply proper ARIA attributes: Include appropriate ARIA attributes for accessibility, such as
aria-expanded
,aria-haspopup
, and others as needed. -
Optimize rendering: Only render complex popper content when needed, and consider lazy loading for heavy content.
-
Use transitions thoughtfully: Apply transitions for a better user experience, but keep them short (200-300ms) to maintain responsiveness.
-
Handle edge cases: Consider edge cases like screen boundaries, scrolling, and window resizing in your implementation.
Common Issues and Their Solutions
Issue: Popper disappears when clicking inside it
This happens because the click event bubbles up and triggers the button's click handler, toggling the popper closed.
Solution:
import React, { useState, useRef } from 'react';
import { Box, Button, Popper, Paper, Typography } from '@mui/material';
function StopPropagationPopper() {
const [open, setOpen] = useState(false);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
// Stop propagation to prevent closing
const handlePopperClick = (event) => {
event.stopPropagation();
};
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
>
Toggle Popper
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
>
<Paper
sx={{ p: 2, width: 250 }}
onClick={handlePopperClick} // Stop propagation here
>
<Typography>
Click inside this popper. It won't close because we're stopping event propagation.
</Typography>
</Paper>
</Popper>
</Box>
);
}
export default StopPropagationPopper;
Issue: Z-index conflicts with other elements
Sometimes the popper might appear behind other elements due to stacking context issues.
Solution:
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
sx={{ zIndex: 1300 }} // Use a higher z-index
>
<Paper sx={{ p: 2 }}>
<Typography>This popper has a higher z-index.</Typography>
</Paper>
</Popper>
Issue: Popper position not updating when anchor element changes size
If your anchor element changes size, the popper might not reposition automatically.
Solution:
import React, { useState, useRef, useEffect } from 'react';
import { Box, Button, Popper, Paper, Typography } from '@mui/material';
function DynamicAnchorPopper() {
const [open, setOpen] = useState(false);
const [expanded, setExpanded] = useState(false);
const [popperInstance, setPopperInstance] = useState(null);
const anchorRef = useRef(null);
const handleToggle = () => {
setOpen((prevOpen) => !prevOpen);
};
const handleExpandClick = () => {
setExpanded((prev) => !prev);
};
// Update popper position when anchor size changes
useEffect(() => {
if (popperInstance) {
popperInstance.update();
}
}, [expanded, popperInstance]);
return (
<Box sx={{ m: 2 }}>
<Button
ref={anchorRef}
variant="contained"
onClick={handleToggle}
sx={{
width: expanded ? 300 : 150,
transition: 'width 0.3s ease'
}}
>
{expanded ? 'Expanded Button' : 'Button'}
</Button>
{open && (
<>
<Button
onClick={handleExpandClick}
sx={{ ml: 2 }}
>
{expanded ? 'Shrink' : 'Expand'} Anchor
</Button>
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
popperRef={setPopperInstance}
>
<Paper sx={{ p: 2, width: 200 }}>
<Typography>
The anchor button changes size, but the popper stays correctly positioned.
</Typography>
</Paper>
</Popper>
</>
)}
</Box>
);
}
export default DynamicAnchorPopper;
Issue: Popper repositioning causes layout shifts
When a popper changes position (e.g., flips from bottom to top), it can cause jarring layout shifts.
Solution:
<Popper
open={open}
anchorEl={anchorRef.current}
placement="bottom"
modifiers={[
{
name: 'flip',
options: {
fallbackPlacements: ['top', 'right', 'left'],
// Add padding to make the flip less jarring
padding: { top: 20, bottom: 20, left: 20, right: 20 },
},
},
{
name: 'preventOverflow',
options: {
// Add margin to prevent edge-to-edge positioning
padding: 8,
},
},
]}
>
<Paper sx={{ p: 2 }}>
<Typography>This popper has smoother repositioning.</Typography>
</Paper>
</Popper>
Wrapping Up
The MUI Popper component is a powerful tool for creating precisely positioned floating elements in your React applications. We've explored its core functionality, built practical UI components, and delved into advanced techniques for optimizing performance and handling edge cases.
By leveraging the Popper component, you can create sophisticated UI elements like tooltips, dropdown menus, and interactive popovers that enhance your application's user experience. Remember to consider accessibility, performance, and edge cases in your implementations to create robust and user-friendly interfaces.
Whether you're building a simple tooltip or a complex interactive popover, the techniques covered in this guide will help you create polished, professional UI components that work reliably across different devices and screen sizes.