Make and Style a Material UI Button With Dropdown Menu

MUI has a great example in the docs of creating a basic MUI dropdown button. In this post I take that example and customize the position of the dropdown and the styling of the dropdown and button.

Here’s what we will build:

MUI Button Dropdown Example
MUI Button Dropdown Example

The button style changes based on the open state of the dropdown. The dropdown position is customized to the location of the button, even if it is dynamic.

The button with dropdown may also be known as a MenuButton.

How to Style the Button

The Button component is a standard MUI Button. However, I added some unique features to give it the visual feel of a dropdown button.

The primary feature is the endIcon. This observes the open state value and renders a different icon depending on state.

Additionally, I created a nested selector in the sx prop that targets MuiButton-endIcon and styles the endIcon. Finally, I set a background color on the button that is only applied when the dropdown is open.

<Button 
  ref={buttonComponent} 
  onClick={handleClick} 
  variant="outlined" 
  sx={{ maxWidth: 200, backgroundColor: open ? 'rgba(0,0,0,0.2)' : "", "& .MuiButton-endIcon": { color: "gold"} }} 
  endIcon={open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}
>
  DropDown Button
</Button>

How to Position the Dropdown Menu

The Menu has a few built-in positioning props. We can also use CSS values and React hooks to accomplish precise and dynamic styling.

Position with anchorOrigin and transformOrigin Props

The “MUI” way of positioning a dropdown below a button (or any other anchor element) is with anchorEl, anchorOrigin, and transformOrigin. These are props built into the Menu component.

Take a look at the code for this example:

const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const open = Boolean(anchorEl);
// Button click handler
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
  setAnchorEl(event.currentTarget);
};
const handleClose = () => {
  setAnchorEl(null);
};

//JSX
<Menu
  anchorEl={anchorEl}
  anchorOrigin={{
    vertical: 'top',
    horizontal: 'right',
  }}
  transformOrigin={{ horizontal: 'left', vertical: 'bottom' }}
/>

The anchorEl prop simply tells the Menu which HTML element to anchor to.

The anchorOrigin prop tells the popper which part of the anchor to attach to. The transformOrigin prop tells the popper which part of the popper will attach to the anchor.

Here’s an example of anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: “top”:

MUI anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: "top"
MUI anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: “top”

And here’s an example of anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: “bottom”:

MUI anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: "bottom"
MUI anchorOrigin.vertical: "bottom" and `transformOrigin.vertical: “bottom”

Notice how the arrow location should be updated depending on anchorOrigin and transformOrigin settings.

Position with PaperProps

Another effective and simple method for styling the Menu is using Menu PaperProps. An sx prop with margin and top, right, bottom, and left styling can quickly adjust the Menu.

Here’s an example:

MUI button dropdown marginLeft: 3 and top: 0
MUI button dropdown marginLeft: 3 and top: 0

This method is most useful for adding spacing with margin. This increases the whitespace between the dropdown and the button.

The top, right, bottom, and left values are will absolutely position relative to the viewport since the dropdown floats in the DOM and is not a child of the button. In the next section I will show how to fix this.

MUI Button Dropdown DOM
MUI Button Dropdown DOM

Position with useEffect and getBoundingClientRect

The positioning values of top, right, bottom, and left are very useful when used with dynamic button position values. We can extract the button position values with getBoundingClientRect.

const buttonComponent = useRef<HTMLButtonElement>(null);
const [position, setPosition] = useState(0);

useEffect(() => {
  setPosition(buttonComponent.current? (buttonComponent.current.getBoundingClientRect().right + 12): 0);
}, [buttonComponent]);

In the code above, I use the useEffect and useRef hooks to get the current right position of the button. Then I add an additional 12 pixels and save that value to a state value called position.

In Menu’s PaperProps, I use this value for the left value of the Menu:

PaperProps={{
  sx: {    
    left: `${position}px !important`,
  }
}}

In this case I needed to add !important. It’s rare that this is needed for styling MUI components, but the Menu had some styling on it that could not be overridden without using the important keyword.

How to Style the Dropdown Menu

The PaperProps sx value is useful for styling the dropdown and it’s contents. For example, I targeted MuiListItemIcon-root and MuiMenuItem-root classes to style icons and text color of each option.

'& .MuiListItemIcon-root': {
  mr: 1,
  color: "primary.main"
},
"& .MuiMenuItem-root ": {
  color: "rgba(0,0,0,0.8)"
}

Another styling feature that I recommend is box shadow. This can go at the root level inside sx PaperProps. The boxShadow prop can pull a value from the MUI theme.

The example from the documentation included the arrow pointer that is injected into the before pseudo class. This was a cool visual trick and you can see it in my complete code for this tutorial in the resources section.

Resources

Full code for this example:

import { useEffect, useRef, useState } from "react";
import { Button, Divider, ListItemIcon, Menu, MenuItem } from "@mui/material";
import CalendarTodayIcon from '@mui/icons-material/CalendarToday';
import EngineeringIcon from '@mui/icons-material/Engineering';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';

export default function DropdownButton() {
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };
  const buttonComponent = useRef<HTMLButtonElement>(null);
  const [position, setPosition] = useState(0);

  useEffect(() => {
    setPosition(buttonComponent.current? (buttonComponent.current.getBoundingClientRect().right + 12): 0);
  }, [buttonComponent]);
  return (
    <>
      <Button ref={buttonComponent} onClick={handleClick} variant="outlined" sx={{ width: 200, backgroundColor: open ? 'rgba(0,0,0,0.2)' : "", "& .MuiButton-endIcon": { color: "gold"} }} endIcon={open ? <KeyboardArrowUpIcon /> : <KeyboardArrowDownIcon />}>DropDown Button</Button>
      <Menu
        anchorEl={anchorEl}
        anchorOrigin={{
          vertical: 'top',
          horizontal: 'right',
        }}
        transformOrigin={{ horizontal: 'left', vertical: 'top' }}
        open={open}
        onClose={handleClose}
        onClick={handleClose}
        PaperProps={{
          sx: {
            zIndex: 1,
            left: `${position}px !important`,
            overflow: 'visible',
            boxShadow: 9,
            mt: 1,
            ml: 3,
            '& .MuiListItemIcon-root': {
              mr: 1,
              color: "primary.main"
            },
            "& .MuiMenuItem-root ": {
              color: "rgba(0,0,0,0.8)"
            },
            '&:before': {
              content: '""',
              display: 'block',
              position: 'absolute',
              top: 14,
              left: -5,
              width: 10,
              height: 10,
              backgroundColor: "background.paper",
              transform: 'translateY(-50%) rotate(45deg)',
              zIndex: 0,
            },
          },
        }}
      >
        <MenuItem>
          Item 1
        </MenuItem>
        <MenuItem>
           Item 2
        </MenuItem>
        <MenuItem>
           Item 3
        </MenuItem>
        <Divider />
        <MenuItem>
          <ListItemIcon>
            <CalendarTodayIcon />
          </ListItemIcon>
          Scheduling
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <EngineeringIcon />
          </ListItemIcon>
          Maintenance
        </MenuItem>
      </Menu>
    </>
  );
}
Share this post:

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.