Menu

일시적인 표면 위에 선택지 목록을 보여주는 메뉴 컴포넌트를 알아볼게요. 사용자가 버튼이나 다른 컨트롤과 상호작용할 때 나타나는 드롭다운 스타일의 UI를 만들 때 사용해요.

출처: 문서

본문

메뉴는 일시적인 표면에 선택지 목록을 표시해요. 사용자가 버튼이나 다른 컨트롤과 상호작용할 때 나타나요.

소개 (Introduction)

메뉴는 관련 컴포넌트 모음으로 구현돼요:

  • Menu: 메뉴의 컨테이너/표면.
  • Menu Item: 사용자가 메뉴에서 선택할 수 있는 옵션.
  • Menu List (선택사항): Menu Item을 위한 대체 합성(composable) 컨테이너 — 자세한 내용은 Composition with Menu List를 참고하세요.

기본 메뉴 (Basic menu)

기본 메뉴는 기본적으로 앵커 요소 위에 열려요(이 옵션은 prop으로 변경할 수 있어요). 화면 가장자리에 가까우면 기본 메뉴는 모든 메뉴 항목이 완전히 보이도록 세로로 재정렬해요.

선택하는 즉시 옵션을 확인하고 메뉴를 닫도록 컴포넌트를 구성해야 해요, 아래 데모에서처럼요.

import * as React from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';

export default function BasicMenu() {
  const id = React.useId();
  const buttonId = `${id}-button`;
  const menuId = `${id}-menu`;
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <Button
        id={buttonId}
        aria-controls={open ? menuId : undefined}
        aria-haspopup="true"
        aria-expanded={open}
        onClick={handleClick}
      >
        Dashboard
      </Button>
      <Menu
        id={menuId}
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
        slotProps={{
          list: {
            'aria-labelledby': buttonId,
          },
        }}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

아이콘 메뉴 (Icon menu)

데스크톱 뷰포트에서는 메뉴에 더 많은 공간을 주기 위해 padding이 증가해요.

import Divider from '@mui/material/Divider';
import Paper from '@mui/material/Paper';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
import Typography from '@mui/material/Typography';
import ContentCut from '@mui/icons-material/ContentCut';
import ContentCopy from '@mui/icons-material/ContentCopy';
import ContentPaste from '@mui/icons-material/ContentPaste';
import Cloud from '@mui/icons-material/Cloud';

export default function IconMenu() {
  return (
    <Paper sx={{ width: 320, maxWidth: '100%' }}>
      <MenuList>
        <MenuItem>
          <ListItemIcon>
            <ContentCut fontSize="small" />
          </ListItemIcon>
          <ListItemText>Cut</ListItemText>
          <Typography variant="body2" sx={{ color: 'text.secondary' }}>
            ⌘X
          </Typography>
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <ContentCopy fontSize="small" />
          </ListItemIcon>
          <ListItemText>Copy</ListItemText>
          <Typography variant="body2" sx={{ color: 'text.secondary' }}>
            ⌘C
          </Typography>
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <ContentPaste fontSize="small" />
          </ListItemIcon>
          <ListItemText>Paste</ListItemText>
          <Typography variant="body2" sx={{ color: 'text.secondary' }}>
            ⌘V
          </Typography>
        </MenuItem>
        <Divider />
        <MenuItem>
          <ListItemIcon>
            <Cloud fontSize="small" />
          </ListItemIcon>
          <ListItemText>Web Clipboard</ListItemText>
        </MenuItem>
      </MenuList>
    </Paper>
  );
}

밀집 메뉴 (Dense menu)

긴 목록과 긴 텍스트가 있는 메뉴에는 dense prop을 사용해서 padding과 텍스트 크기를 줄일 수 있어요.

import Paper from '@mui/material/Paper';
import Divider from '@mui/material/Divider';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import Check from '@mui/icons-material/Check';

export default function DenseMenu() {
  return (
    <Paper sx={{ width: 320 }}>
      <MenuList dense>
        <MenuItem>
          <ListItemText inset>Single</ListItemText>
        </MenuItem>
        <MenuItem>
          <ListItemText inset>1.15</ListItemText>
        </MenuItem>
        <MenuItem>
          <ListItemText inset>Double</ListItemText>
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <Check />
          </ListItemIcon>
          Custom: 1.2
        </MenuItem>
        <Divider />
        <MenuItem>
          <ListItemText>Add space before paragraph</ListItemText>
        </MenuItem>
        <MenuItem>
          <ListItemText>Add space after paragraph</ListItemText>
        </MenuItem>
        <Divider />
        <MenuItem>
          <ListItemText>Custom spacing…</ListItemText>
        </MenuItem>
      </MenuList>
    </Paper>
  );
}

선택된 메뉴 (Selected menu)

항목 선택에 사용된다면, 열릴 때 단순 메뉴는 초기 포커스를 선택된 메뉴 항목에 둬요. 현재 선택된 메뉴 항목은 MenuItem에서 사용 가능한 selected prop으로 설정돼요. 초기 포커스에 영향을 주지 않고 선택된 메뉴 항목을 사용하려면 variant prop을 "menu"로 설정해요.

import * as React from 'react';
import List from '@mui/material/List';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemText from '@mui/material/ListItemText';
import MenuItem from '@mui/material/MenuItem';
import Menu from '@mui/material/Menu';

const options = [
  'Show some love to MUI',
  'Show all notification content',
  'Hide sensitive notification content',
  'Hide all notification content',
];

export default function SimpleListMenu() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const [selectedIndex, setSelectedIndex] = React.useState(1);
  const open = Boolean(anchorEl);
  const handleClickListItem = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };

  const handleMenuItemClick = (
    event: React.MouseEvent<HTMLElement>,
    index: number,
  ) => {
    setSelectedIndex(index);
    setAnchorEl(null);
  };

  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <List
        component="nav"
        aria-label="Device settings"
        sx={{ bgcolor: 'background.paper' }}
      >
        <ListItemButton
          id="lock-button"
          aria-haspopup="listbox"
          aria-controls="lock-menu"
          aria-label="when device is locked"
          aria-expanded={open}
          onClick={handleClickListItem}
        >
          <ListItemText
            primary="When device is locked"
            secondary={options[selectedIndex]}
          />
        </ListItemButton>
      </List>
      <Menu
        id="lock-menu"
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
        slotProps={{
          list: {
            'aria-labelledby': 'lock-button',
            role: 'listbox',
          },
        }}
      >
        {options.map((option, index) => (
          <MenuItem
            key={option}
            disabled={index === 0}
            selected={index === selectedIndex}
            onClick={(event) => handleMenuItemClick(event, index)}
          >
            {option}
          </MenuItem>
        ))}
      </Menu>
    </div>
  );
}

체크박스 및 라디오 메뉴 항목 (Checkbox and radio menu items)

토글 가능한 옵션 메뉴를 만들려면, 각 항목의 role을 독립적인 토글에는 menuitemcheckbox로, 그룹 내 단일 선택에는 menuitemradio로 설정해요. 이 역할들에서는 selected prop이 aria-checked를 구동하므로, 보조 기술이 체크 상태를 알려줘요.

import * as React from 'react';
import Paper from '@mui/material/Paper';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
import Check from '@mui/icons-material/Check';

const options = ['Show toolbar', 'Show sidebar', 'Show status bar'];

export default function CheckboxMenu() {
  const [checked, setChecked] = React.useState<Record<string, boolean>>({
    'Show toolbar': true,
  });

  const handleToggle = (option: string) => () => {
    setChecked((prev) => ({ ...prev, [option]: !prev[option] }));
  };

  return (
    <Paper sx={{ width: 320, maxWidth: '100%' }}>
      <MenuList>
        {options.map((option) => (
          <MenuItem
            key={option}
            role="menuitemcheckbox"
            selected={Boolean(checked[option])}
            onClick={handleToggle(option)}
          >
            <ListItemIcon>
              {checked[option] ? <Check fontSize="small" /> : null}
            </ListItemIcon>
            <ListItemText>{option}</ListItemText>
          </MenuItem>
        ))}
      </MenuList>
    </Paper>
  );
}

그룹 내 단일 선택에는 menuitemradio를 사용해요:

import * as React from 'react';
import Paper from '@mui/material/Paper';
import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import ListItemText from '@mui/material/ListItemText';
import ListItemIcon from '@mui/material/ListItemIcon';
import RadioButtonChecked from '@mui/icons-material/RadioButtonChecked';
import RadioButtonUnchecked from '@mui/icons-material/RadioButtonUnchecked';

const options = ['Name', 'Date modified', 'Size'];

export default function RadioMenu() {
  const [selected, setSelected] = React.useState('Name');

  return (
    <Paper sx={{ width: 320, maxWidth: '100%' }}>
      <MenuList>
        {options.map((option) => (
          <MenuItem
            key={option}
            role="menuitemradio"
            selected={selected === option}
            onClick={() => setSelected(option)}
          >
            <ListItemIcon>
              {selected === option ? (
                <RadioButtonChecked fontSize="small" />
              ) : (
                <RadioButtonUnchecked fontSize="small" />
              )}
            </ListItemIcon>
            <ListItemText>{option}</ListItemText>
          </MenuItem>
        ))}
      </MenuList>
    </Paper>
  );
}

위치 지정 메뉴 (Positioned menu)

Menu 컴포넌트는 위치를 지정하기 위해 Popover 컴포넌트를 사용하므로, 같은 positioning props를 사용해서 위치를 지정할 수 있어요. 예를 들어 앵커 위에 메뉴를 표시할 수 있어요:

import * as React from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';

export default function PositionedMenu() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <Button
        id="demo-positioned-button"
        aria-controls={open ? 'demo-positioned-menu' : undefined}
        aria-haspopup="true"
        aria-expanded={open}
        onClick={handleClick}
      >
        Dashboard
      </Button>
      <Menu
        id="demo-positioned-menu"
        aria-labelledby="demo-positioned-button"
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
        anchorOrigin={{
          vertical: 'top',
          horizontal: 'left',
        }}
        transformOrigin={{
          vertical: 'top',
          horizontal: 'left',
        }}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

Menu 컴포넌트는 내부적으로 Popover 컴포넌트를 사용해요. 하지만 다른 위치 지정 전략을 사용하고 싶거나, 예를 들어 스크롤을 차단하지 않기를 원할 수 있어요.

Menu List 컴포넌트는 이런 사용 사례에 자신만의 메뉴를 구성할 수 있게 해줘요 — 그 주된 목적은 포커스를 처리하는 거예요. 아래 데모는 Menu List를 사용하고 Menu의 기본 Popover 대신 Popper 컴포넌트를 사용한 합성 예제예요:

import * as React from 'react';
import Button from '@mui/material/Button';
import ClickAwayListener from '@mui/material/ClickAwayListener';
import Grow from '@mui/material/Grow';
import Paper from '@mui/material/Paper';
import Popper from '@mui/material/Popper';
import MenuItem from '@mui/material/MenuItem';
import MenuList from '@mui/material/MenuList';
import Stack from '@mui/material/Stack';

export default function MenuListComposition() {
  const [open, setOpen] = React.useState(false);
  const anchorRef = React.useRef<HTMLButtonElement>(null);

  const handleToggle = () => {
    setOpen((prevOpen) => !prevOpen);
  };

  const handleClose = (event: Event | React.SyntheticEvent) => {
    if (
      anchorRef.current &&
      anchorRef.current.contains(event.target as HTMLElement)
    ) {
      return;
    }

    setOpen(false);
  };

  function handleListKeyDown(event: React.KeyboardEvent) {
    if (event.key === 'Tab') {
      event.preventDefault();
      setOpen(false);
    } else if (event.key === 'Escape') {
      setOpen(false);
    }
  }

  // return focus to the button when we transitioned from !open -> open
  const prevOpen = React.useRef(open);
  React.useEffect(() => {
    if (prevOpen.current === true && open === false) {
      anchorRef.current!.focus();
    }

    prevOpen.current = open;
  }, [open]);

  return (
    <Stack direction="row" spacing={2}>
      <Paper>
        <MenuList>
          <MenuItem>Profile</MenuItem>
          <MenuItem>My account</MenuItem>
          <MenuItem>Logout</MenuItem>
        </MenuList>
      </Paper>
      <div>
        <Button
          ref={anchorRef}
          id="composition-button"
          aria-controls={open ? 'composition-menu' : undefined}
          aria-expanded={open}
          aria-haspopup="true"
          onClick={handleToggle}
        >
          Dashboard
        </Button>
        <Popper
          open={open}
          anchorEl={anchorRef.current}
          role={undefined}
          placement="bottom-start"
          transition
          disablePortal
        >
          {({ TransitionProps, placement }) => (
            <Grow
              {...TransitionProps}
              style={{
                transformOrigin:
                  placement === 'bottom-start' ? 'left top' : 'left bottom',
              }}
            >
              <Paper>
                <ClickAwayListener onClickAway={handleClose}>
                  <MenuList
                    autoFocusItem={open}
                    id="composition-menu"
                    aria-labelledby="composition-button"
                    onKeyDown={handleListKeyDown}
                  >
                    <MenuItem onClick={handleClose}>Profile</MenuItem>
                    <MenuItem onClick={handleClose}>My account</MenuItem>
                    <MenuItem onClick={handleClose}>Logout</MenuItem>
                  </MenuList>
                </ClickAwayListener>
              </Paper>
            </Grow>
          )}
        </Popper>
      </div>
    </Stack>
  );
}

계정 메뉴 (Account menu)

Menu 콘텐츠는 Avatar 같은 다른 컴포넌트와 섞일 수 있어요.

import * as React from 'react';
import Box from '@mui/material/Box';
import Avatar from '@mui/material/Avatar';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import ListItemIcon from '@mui/material/ListItemIcon';
import Divider from '@mui/material/Divider';
import IconButton from '@mui/material/IconButton';
import Typography from '@mui/material/Typography';
import Tooltip from '@mui/material/Tooltip';
import PersonAdd from '@mui/icons-material/PersonAdd';
import Settings from '@mui/icons-material/Settings';
import Logout from '@mui/icons-material/Logout';

export default function AccountMenu() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };
  return (
    <React.Fragment>
      <Box sx={{ display: 'flex', alignItems: 'center', textAlign: 'center' }}>
        <Typography sx={{ minWidth: 100 }}>Contact</Typography>
        <Typography sx={{ minWidth: 100 }}>Profile</Typography>
        <Tooltip title="Account settings">
          <IconButton
            onClick={handleClick}
            size="small"
            sx={{ ml: 2 }}
            aria-controls={open ? 'account-menu' : undefined}
            aria-haspopup="true"
            aria-expanded={open}
          >
            <Avatar sx={{ width: 32, height: 32 }}>M</Avatar>
          </IconButton>
        </Tooltip>
      </Box>
      <Menu
        anchorEl={anchorEl}
        id="account-menu"
        open={open}
        onClose={handleClose}
        onClick={handleClose}
        slotProps={{
          paper: {
            elevation: 0,
            sx: {
              overflow: 'visible',
              filter: 'drop-shadow(0px 2px 8px rgba(0,0,0,0.32))',
              mt: 1.5,
              '& .MuiAvatar-root': {
                width: 32,
                height: 32,
                ml: -0.5,
                mr: 1,
              },
              '&::before': {
                content: '""',
                display: 'block',
                position: 'absolute',
                top: 0,
                right: 14,
                width: 10,
                height: 10,
                bgcolor: 'background.paper',
                transform: 'translateY(-50%) rotate(45deg)',
                zIndex: 0,
              },
            },
          },
        }}
        transformOrigin={{ horizontal: 'right', vertical: 'top' }}
        anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
      >
        <MenuItem onClick={handleClose}>
          <Avatar /> Profile
        </MenuItem>
        <MenuItem onClick={handleClose}>
          <Avatar /> My account
        </MenuItem>
        <Divider />
        <MenuItem onClick={handleClose}>
          <ListItemIcon>
            <PersonAdd fontSize="small" />
          </ListItemIcon>
          Add another account
        </MenuItem>
        <MenuItem onClick={handleClose}>
          <ListItemIcon>
            <Settings fontSize="small" />
          </ListItemIcon>
          Settings
        </MenuItem>
        <MenuItem onClick={handleClose}>
          <ListItemIcon>
            <Logout fontSize="small" />
          </ListItemIcon>
          Logout
        </MenuItem>
      </Menu>
    </React.Fragment>
  );
}

커스터마이즈 (Customization)

컴포넌트를 커스터마이즈하는 예제예요. 이에 대해 더 자세히 알아보려면 overrides 문서 페이지를 참고하세요.

import * as React from 'react';
import { styled, alpha } from '@mui/material/styles';
import Button from '@mui/material/Button';
import Menu, { MenuProps } from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import EditIcon from '@mui/icons-material/Edit';
import Divider from '@mui/material/Divider';
import ArchiveIcon from '@mui/icons-material/Archive';
import FileCopyIcon from '@mui/icons-material/FileCopy';
import MoreHorizIcon from '@mui/icons-material/MoreHoriz';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';

const StyledMenu = styled((props: MenuProps) => (
  <Menu
    elevation={0}
    anchorOrigin={{
      vertical: 'bottom',
      horizontal: 'right',
    }}
    transformOrigin={{
      vertical: 'top',
      horizontal: 'right',
    }}
    {...props}
  />
))(({ theme }) => ({
  '& .MuiPaper-root': {
    borderRadius: 6,
    marginTop: theme.spacing(1),
    minWidth: 180,
    color: 'rgb(55, 65, 81)',
    boxShadow:
      'rgb(255, 255, 255) 0px 0px 0px 0px, rgba(0, 0, 0, 0.05) 0px 0px 0px 1px, rgba(0, 0, 0, 0.1) 0px 10px 15px -3px, rgba(0, 0, 0, 0.05) 0px 4px 6px -2px',
    '& .MuiMenu-list': {
      padding: '4px 0',
    },
    '& .MuiMenuItem-root': {
      '& .MuiSvgIcon-root': {
        fontSize: 18,
        color: theme.palette.text.secondary,
        marginRight: theme.spacing(1.5),
        ...theme.applyStyles('dark', {
          color: 'inherit',
        }),
      },
      '&:active': {
        backgroundColor: alpha(
          theme.palette.primary.main,
          theme.palette.action.selectedOpacity,
        ),
      },
    },
    ...theme.applyStyles('dark', {
      color: theme.palette.grey[300],
    }),
  },
}));

export default function CustomizedMenus() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <Button
        id="demo-customized-button"
        aria-controls={open ? 'demo-customized-menu' : undefined}
        aria-haspopup="true"
        aria-expanded={open}
        variant="contained"
        disableElevation
        onClick={handleClick}
        endIcon={<KeyboardArrowDownIcon />}
      >
        Options
      </Button>
      <StyledMenu
        id="demo-customized-menu"
        slotProps={{
          list: {
            'aria-labelledby': 'demo-customized-button',
          },
        }}
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
      >
        <MenuItem onClick={handleClose} disableRipple>
          <EditIcon />
          Edit
        </MenuItem>
        <MenuItem onClick={handleClose} disableRipple>
          <FileCopyIcon />
          Duplicate
        </MenuItem>
        <Divider sx={{ my: 0.5 }} />
        <MenuItem onClick={handleClose} disableRipple>
          <ArchiveIcon />
          Archive
        </MenuItem>
        <MenuItem onClick={handleClose} disableRipple>
          <MoreHorizIcon />
          More
        </MenuItem>
      </StyledMenu>
    </div>
  );
}

MenuItem은 몇 가지 추가 스타일이 있는 ListItem의 래퍼예요. MenuItem 컴포넌트로도 동일한 리스트 합성 기능을 사용할 수 있어요:

🎨 영감을 찾고 있다면, MUI Treasury의 커스터마이즈 예제를 확인해 보세요.

최대 높이 메뉴 (Max height menu)

메뉴의 높이 때문에 모든 메뉴 항목을 표시할 수 없다면, 메뉴가 내부적으로 스크롤할 수 있어요.

import * as React from 'react';
import IconButton from '@mui/material/IconButton';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import MoreVertIcon from '@mui/icons-material/MoreVert';

const options = [
  'None',
  'Atria',
  'Callisto',
  'Dione',
  'Ganymede',
  'Hangouts Call',
  'Luna',
  'Oberon',
  'Phobos',
  'Pyxis',
  'Sedna',
  'Titania',
  'Triton',
  'Umbriel',
];

const ITEM_HEIGHT = 48;

export default function LongMenu() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <IconButton
        aria-label="more"
        id="long-button"
        aria-controls={open ? 'long-menu' : undefined}
        aria-expanded={open}
        aria-haspopup="true"
        onClick={handleClick}
      >
        <MoreVertIcon />
      </IconButton>
      <Menu
        id="long-menu"
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
        slotProps={{
          paper: {
            style: {
              maxHeight: ITEM_HEIGHT * 4.5,
              width: '20ch',
            },
          },
          list: {
            'aria-labelledby': 'long-button',
          },
        }}
      >
        {options.map((option) => (
          <MenuItem key={option} selected={option === 'Pyxis'} onClick={handleClose}>
            {option}
          </MenuItem>
        ))}
      </Menu>
    </div>
  );
}

제한 사항 (Limitations)

flexbox 레이아웃에서 text-overflow: ellipsis가 작동하지 못하게 하는 flexbox 버그가 있어요. noWrap과 함께 Typography 컴포넌트를 사용해서 이 문제를 해결할 수 있어요:

import MenuList from '@mui/material/MenuList';
import MenuItem from '@mui/material/MenuItem';
import Paper from '@mui/material/Paper';
import ListItemIcon from '@mui/material/ListItemIcon';
import Typography from '@mui/material/Typography';
import DraftsIcon from '@mui/icons-material/Drafts';
import SendIcon from '@mui/icons-material/Send';
import PriorityHighIcon from '@mui/icons-material/PriorityHigh';

export default function TypographyMenu() {
  return (
    <Paper sx={{ width: 230 }}>
      <MenuList>
        <MenuItem>
          <ListItemIcon>
            <SendIcon fontSize="small" />
          </ListItemIcon>
          <Typography variant="inherit">A short message</Typography>
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <PriorityHighIcon fontSize="small" />
          </ListItemIcon>
          <Typography variant="inherit">A very long text that overflows</Typography>
        </MenuItem>
        <MenuItem>
          <ListItemIcon>
            <DraftsIcon fontSize="small" />
          </ListItemIcon>
          <Typography variant="inherit" noWrap>
            A very long text that overflows
          </Typography>
        </MenuItem>
      </MenuList>
    </Paper>
  );
}

전환 변경 (Change transition)

slots.transition과 slotProps.transition을 사용해서 다른 전환을 사용할 수 있어요.

import * as React from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Fade from '@mui/material/Fade';

export default function FadeMenu() {
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <Button
        id="fade-button"
        aria-controls={open ? 'fade-menu' : undefined}
        aria-haspopup="true"
        aria-expanded={open}
        onClick={handleClick}
      >
        Dashboard
      </Button>
      <Menu
        id="fade-menu"
        slotProps={{
          list: {
            'aria-labelledby': 'fade-button',
          },
        }}
        slots={{ transition: Fade }}
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
      >
        <MenuItem onClick={handleClose}>Profile</MenuItem>
        <MenuItem onClick={handleClose}>My account</MenuItem>
        <MenuItem onClick={handleClose}>Logout</MenuItem>
      </Menu>
    </div>
  );
}

컨텍스트 메뉴 (Context menu)

컨텍스트 메뉴의 예제예요. (마우스 오른쪽 클릭으로 열어요.)

import * as React from 'react';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import Typography from '@mui/material/Typography';

export default function ContextMenu() {
  const [contextMenu, setContextMenu] = React.useState<{
    mouseX: number;
    mouseY: number;
  } | null>(null);

  const handleContextMenu = (event: React.MouseEvent) => {
    event.preventDefault();

    setContextMenu(
      contextMenu === null
        ? {
            mouseX: event.clientX + 2,
            mouseY: event.clientY - 6,
          }
        : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu
          // Other native context menus might behave different.
          // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus.
          null,
    );

    // Prevent text selection lost after opening the context menu on Safari and Firefox
    const selection = document.getSelection();
    if (selection && selection.rangeCount > 0) {
      const range = selection.getRangeAt(0);

      setTimeout(() => {
        selection.addRange(range);
      });
    }
  };

  const handleClose = () => {
    setContextMenu(null);
  };

  return (
    <div onContextMenu={handleContextMenu} style={{ cursor: 'context-menu' }}>
      <Typography>
        Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ipsum purus,
        bibendum sit amet vulputate eget, porta semper ligula. Donec bibendum
        vulputate erat, ac fringilla mi finibus nec. Donec ac dolor sed dolor
        porttitor blandit vel vel purus. Fusce vel malesuada ligula. Nam quis
        vehicula ante, eu finibus est. Proin ullamcorper fermentum orci, quis finibus
        massa. Nunc lobortis, massa ut rutrum ultrices, metus metus finibus ex, sit
        amet facilisis neque enim sed neque. Quisque accumsan metus vel maximus
        consequat. Suspendisse lacinia tellus a libero volutpat maximus.
      </Typography>
      <Menu
        open={contextMenu !== null}
        onClose={handleClose}
        anchorReference="anchorPosition"
        anchorPosition={
          contextMenu !== null
            ? { top: contextMenu.mouseY, left: contextMenu.mouseX }
            : undefined
        }
      >
        <MenuItem onClick={handleClose}>Copy</MenuItem>
        <MenuItem onClick={handleClose}>Print</MenuItem>
        <MenuItem onClick={handleClose}>Highlight</MenuItem>
        <MenuItem onClick={handleClose}>Email</MenuItem>
      </Menu>
    </div>
  );
}

그룹 메뉴 (Grouped Menu)

ListSubheader 컴포넌트로 카테고리를 표시해요.

import * as React from 'react';
import Button from '@mui/material/Button';
import ListSubheader from '@mui/material/ListSubheader';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import { styled } from '@mui/material/styles';

const StyledListHeader = styled(ListSubheader)({
  backgroundImage: 'var(--Paper-overlay)',
});

export default function GroupedMenu() {
  const id = React.useId();
  const buttonId = `${id}-button`;
  const menuId = `${id}-menu`;
  const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
  const open = Boolean(anchorEl);
  const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
    setAnchorEl(event.currentTarget);
  };
  const handleClose = () => {
    setAnchorEl(null);
  };

  return (
    <div>
      <Button
        id={buttonId}
        aria-controls={open ? menuId : undefined}
        aria-haspopup="true"
        aria-expanded={open}
        onClick={handleClick}
      >
        Dashboard
      </Button>
      <Menu
        id={menuId}
        anchorEl={anchorEl}
        open={open}
        onClose={handleClose}
        slotProps={{
          list: {
            'aria-labelledby': buttonId,
            sx: {
              py: 0,
            },
          },
        }}
      >
        <StyledListHeader>Category 1</StyledListHeader>
        <MenuItem onClick={handleClose}>Option 1</MenuItem>
        <MenuItem onClick={handleClose}>Option 2</MenuItem>
        <StyledListHeader>Category 2</StyledListHeader>
        <MenuItem onClick={handleClose}>Option 3</MenuItem>
        <MenuItem onClick={handleClose}>Option 4</MenuItem>
      </Menu>
    </div>
  );
}

보조 프로젝트 (Supplementary projects)

더 고급 사용 사례를 위해 다음을 활용할 수 있어요:

material-ui-popup-state

stars npm downloads

대부분의 경우 메뉴 상태를 대신 처리해주는 material-ui-popup-state 패키지예요.

import * as React from 'react';
import Button from '@mui/material/Button';
import Menu from '@mui/material/Menu';
import MenuItem from '@mui/material/MenuItem';
import PopupState, { bindTrigger, bindMenu } from 'material-ui-popup-state';

export default function MenuPopupState() {
  return (
    <PopupState variant="popover" popupId="demo-popup-menu">
      {(popupState) => (
        <React.Fragment>
          <Button variant="contained" {...bindTrigger(popupState)}>
            Dashboard
          </Button>
          <Menu {...bindMenu(popupState)}>
            <MenuItem onClick={popupState.close}>Profile</MenuItem>
            <MenuItem onClick={popupState.close}>My account</MenuItem>
            <MenuItem onClick={popupState.close}>Logout</MenuItem>
          </Menu>
        </React.Fragment>
      )}
    </PopupState>
  );
}

ClickAwayListener API

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import ClickAwayListener from '@mui/material/ClickAwayListener';
// or
import { ClickAwayListener } from '@mui/material';

Props

Name Type Default Required Description
children element - Yes
onClickAway func - Yes
disableReactTree bool false No
mouseEvent 'onClick' | 'onMouseDown' | 'onMouseUp' | 'onPointerDown' | 'onPointerUp' | false 'onClick' No
touchEvent 'onTouchEnd' | 'onTouchStart' | false 'onTouchEnd' No

Note: The ref is forwarded to the root element.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import Menu from '@mui/material/Menu';
// or
import { Menu } from '@mui/material';

Props

Name Type Default Required Description
open bool - Yes
anchorEl HTML element | func - No
autoFocus bool true No
children node - No
classes object - No Override or extend the styles applied to the component.
disableAutoFocusItem bool false No
onClose function(event: object, reason: string) => void - No
PopoverClasses object - No
slotProps { backdrop?: func | object, list?: func | object, paper?: func | object, root?: func | object, transition?: func | object } {} No
slots { backdrop?: elementType, list?: elementType, paper?: elementType, root?: elementType, transition?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
transitionDuration 'auto' | number | { appear?: number, enter?: number, exit?: number } 'auto' No
variant 'menu' | 'selectedMenu' 'selectedMenu' No

Note: The ref is forwarded to the root element (HTMLDivElement).

Any other props supplied will be provided to the root element (Popover).

Inheritance (상속)

위에 명시적으로 문서화되지는 않았지만, Popover 컴포넌트의 props도 Menu에서 사용할 수 있어요.

Slots

Name Default Class Description
root Modal .MuiMenu-root The component used for the popper.
paper PopoverPaper .MuiMenu-paper The component used for the paper.
list MenuList .MuiMenu-list The component used for the list.
transition Grow - The component used for the transition slot.
backdrop Backdrop - The component used for the backdrop slot.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import MenuItem from '@mui/material/MenuItem';
// or
import { MenuItem } from '@mui/material';

Props

Name Type Default Required Description
autoFocus bool false No
children node - No
classes object - No Override or extend the styles applied to the component.
component elementType - No
dense bool false No
disableGutters bool false No
divider bool false No
focusVisibleClassName string - No
selected bool false No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.

Note: The ref is forwarded to the root element (HTMLLIElement).

Any other props supplied will be provided to the root element (ButtonBase).

Inheritance (상속)

위에 명시적으로 문서화되지는 않았지만, ButtonBase 컴포넌트의 props도 MenuItem에서 사용할 수 있어요.

Theme default props (테마 기본 props)

MuiMenuItem을 사용해서 테마로 이 컴포넌트의 기본 props를 변경할 수 있어요.

CSS

Rule name (규칙 이름)

Global class Rule name Description
- dense Styles applied to the root element if dense.
.Mui-disabled - State class applied to the root element if disabled={true}.
- divider Styles applied to the root element if divider={true}.
.Mui-focusVisible - State class applied to the root element if keyboard focused.
- gutters Styles applied to the inner component element unless disableGutters={true}.
- root Styles applied to the root element.
.Mui-selected - State class applied to the root element if selected={true}.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import MenuList from '@mui/material/MenuList';
// or
import { MenuList } from '@mui/material';

Props

Name Type Default Required Description
autoFocus bool false No
autoFocusItem bool false No
children node - No
disabledItemsFocusable bool false No
disableListWrap bool false No
variant 'menu' | 'selectedMenu' 'selectedMenu' No

Note: The ref is forwarded to the root element (HTMLUListElement).

Any other props supplied will be provided to the root element (List).

Inheritance (상속)

위에 명시적으로 문서화되지는 않았지만, List 컴포넌트의 props도 MenuList에서 사용할 수 있어요.

CSS

Rule name (규칙 이름)

Global class Rule name Description
- dense Styles applied to the root element if dense.
- padding Styles applied to the root element unless disablePadding={true}.
- root Styles applied to the root element.
- subheader Styles applied to the root element if a subheader is provided.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

Popover API

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import Popover from '@mui/material/Popover';
// or
import { Popover } from '@mui/material';

Props

Name Type Default Required Description
open bool - Yes
action ref - No
anchorEl HTML element | func - No
anchorOrigin { horizontal: 'center' | 'left' | 'right' | number, vertical: 'bottom' | 'center' | 'top' | number } `{
vertical: 'top',
horizontal: 'left',
}` No
anchorPosition { left: number, top: number } - No
anchorReference 'anchorEl' | 'anchorPosition' | 'none' 'anchorEl' No
children node - No
classes object - No Override or extend the styles applied to the component.
container HTML element | func - No
disableAutoFocus bool false No
disableScrollLock bool false No
elevation integer 8 No
marginThreshold number 16 No
onClose func - No
slotProps { backdrop?: func | object, paper?: func | object, root?: func | object, transition?: func | object } {} No
slots { backdrop?: elementType, paper?: elementType, root?: elementType, transition?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
transformOrigin { horizontal: 'center' | 'left' | 'right' | number, vertical: 'bottom' | 'center' | 'top' | number } `{
vertical: 'top',
horizontal: 'left',
}` No
transitionDuration 'auto' | number | { appear?: number, enter?: number, exit?: number } 'auto' No

Note: The ref is forwarded to the root element (HTMLDivElement).

Any other props supplied will be provided to the root element (Modal).

Inheritance (상속)

위에 명시적으로 문서화되지는 않았지만, Modal 컴포넌트의 props도 Popover에서 사용할 수 있어요.

Slots

Name Default Class Description
root Modal .MuiPopover-root The component used for the root slot.
paper Paper .MuiPopover-paper The component used for the paper slot.
transition Grow - The component used for the transition slot.
backdrop Backdrop - The component used for the backdrop slot.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

Popper API

Demos (데모)

이 React 컴포넌트 사용에 대한 예시와 세부 사항은 컴포넌트 데모 페이지를 방문하세요:

Import (임포트)

import Popper from '@mui/material/Popper';
// or
import { Popper } from '@mui/material';

Props

Name Type Default Required Description
open bool - Yes
anchorEl HTML element | object | func - No
children node | func - No
component elementType - No
container HTML element | func - No
disablePortal bool false No
keepMounted bool false No
modifiers Array<{ data?: object, effect?: func, enabled?: bool, fn?: func, name?: any, options?: object, phase?: 'afterMain' | 'afterRead' | 'afterWrite' | 'beforeMain' | 'beforeRead' | 'beforeWrite' | 'main' | 'read' | 'write', requires?: Array<string>, requiresIfExists?: Array<string> }> - No
placement 'auto-end' | 'auto-start' | 'auto' | 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top' 'bottom' No
popperOptions { modifiers?: array, onFirstUpdate?: func, placement?: 'auto-end' | 'auto-start' | 'auto' | 'bottom-end' | 'bottom-start' | 'bottom' | 'left-end' | 'left-start' | 'left' | 'right-end' | 'right-start' | 'right' | 'top-end' | 'top-start' | 'top', strategy?: 'absolute' | 'fixed' } {} No
popperRef ref - No
slotProps { root?: func | object } {} No
slots { root?: elementType } {} No
sx Array<func | object | bool> | func | object - No The system prop that allows defining system overrides as well as additional CSS styles.
transition bool false No

Note: The ref is forwarded to the root element (HTMLDivElement).

Any other props supplied will be provided to the root element (native element).

CSS

Rule name (규칙 이름)

Global class Rule name Description
- root Class name applied to the root element.

Source code (소스 코드)

이 페이지에서 원하는 정보를 찾지 못했다면, 더 자세한 내용을 위해 컴포넌트의 구현을 살펴보는 것이 좋아요.

더 알아보기 (Learn more)

  • Popover — 메뉴가 위치를 위해 사용하는 컴포넌트
  • Popper — 위치 지정 도구