Menu

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:

  1. Implement basic Popper functionality with various placement options
  2. Create custom positioned components like tooltips and dropdown menus
  3. Handle edge cases with modifiers and transition effects
  4. Apply advanced positioning strategies for complex UI requirements
  5. 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:

PropTypeDefaultDescription
anchorElElement | Object | FunctionnullThe reference element used to position the Popper
childrenNode | Function-The content of the Popper
openBooleanfalseControls the visibility of the Popper
placementString'bottom'Position where the Popper should be displayed
transitionBooleanfalseIf true, adds transition when the Popper mounts/unmounts
disablePortalBooleanfalseDisables using Portal to render children into a new subtree
modifiersArray[]Popper.js modifiers to customize behavior
popperOptionsObjectPass options directly to Popper.js instance
popperRefRef-Ref to get access to the popper instance
keepMountedBooleanfalseAlways 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 anchor
  • top-start - Element above, aligned with the left edge
  • top-end - Element above, aligned with the right edge
  • bottom - Element below the anchor
  • bottom-start - Element below, aligned with the left edge
  • bottom-end - Element below, aligned with the right edge
  • right - Element to the right of the anchor
  • right-start - Element to the right, aligned with the top edge
  • right-end - Element to the right, aligned with the bottom edge
  • left - Element to the left of the anchor
  • left-start - Element to the left, aligned with the top edge
  • left-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 element
  • preventOverflow - Prevents the popper from being positioned outside the boundary
  • offset - Offsets the popper from its reference element

Accessibility Considerations

When using Popper, you need to ensure that your custom UI components remain accessible:

  1. Use appropriate ARIA attributes (aria-expanded, aria-haspopup, etc.)
  2. Ensure keyboard navigation works correctly
  3. Add proper focus management
  4. 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:

  1. We create a state variable open to control the visibility of the popper
  2. We use useRef to create a reference to the button that will trigger the popper
  3. The Popper component uses the anchorRef.current as its anchor element
  4. We set the placement to "bottom" to position the popper below the button
  5. 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:

  1. We added a dropdown to select from the 12 different placement options
  2. The selected placement is passed to the Popper component
  3. The UI displays the current placement for clarity
  4. 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:

  1. Using the transition prop on the Popper component
  2. Applying MUI transition components (Fade and Grow) to the popper content
  3. Toggling between different transition effects
  4. 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:

  1. Uses mouse events to trigger the tooltip display
  2. Applies a fade transition for smooth appearance
  3. Includes an optional arrow that points to the anchor element
  4. Supports all placement options
  5. Uses styled components for consistent styling
  6. 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:

  1. Uses a button with an arrow icon as the trigger
  2. Shows a menu with icons and a divider when clicked
  3. Closes when clicking outside using ClickAwayListener
  4. Handles keyboard navigation (Escape key to close)
  5. Uses a Grow transition for a natural appearance
  6. Includes proper ARIA attributes for accessibility
  7. 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:

  1. Creating a virtual element with the getBoundingClientRect method
  2. Positioning a popper at arbitrary coordinates on the screen
  3. Updating the virtual element when the position changes
  4. 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:

  1. Using the offset modifier to create space between the anchor and popper
  2. Enabling/disabling the preventOverflow modifier to control boundary detection
  3. Toggling the flip modifier to control automatic placement adjustment
  4. Interactive controls to adjust modifier settings in real-time
  5. 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:

  1. Contains a form with input fields and a submit button
  2. Handles form submission and validation
  3. Uses ClickAwayListener to close when clicking outside
  4. Includes a close button in the header
  5. Applies proper styling and spacing for a good UX
  6. 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:

  1. Uses React's lazy loading to only load the popper content when needed
  2. Conditionally renders the popper component based on its visibility
  3. Shows a loading indicator while the content is being loaded
  4. 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:

  1. Creates a debounced update function that limits how often the popper recalculates its position
  2. Uses the popperRef prop to get access to the popper instance
  3. Manually calls the popper's update method after debouncing
  4. Attaches and detaches event listeners appropriately
  5. 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

  1. Always use a controlled pattern: Manage the open state externally for better control and predictability.

  2. Handle focus management: When using Popper for interactive elements like menus, ensure proper focus management for accessibility.

  3. Use ClickAwayListener: For interactive poppers, always include a ClickAwayListener to close the popper when clicking outside.

  4. Apply proper ARIA attributes: Include appropriate ARIA attributes for accessibility, such as aria-expanded, aria-haspopup, and others as needed.

  5. Optimize rendering: Only render complex popper content when needed, and consider lazy loading for heavy content.

  6. Use transitions thoughtfully: Apply transitions for a better user experience, but keep them short (200-300ms) to maintain responsiveness.

  7. 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.