Menu
Menu
보조 작업의 목록을 하나의 대화형 영역으로 결합하는 컴포넌트예요. 보통 메뉴 버튼이나 드롭다운을 만들 때 사용해요.
출처: 문서
본문
사용법 (Usage)
Menu로 보조 작업 목록을 하나의 대화형 영역으로 만들 수 있어요.
import { Menu, Button, Text } from '@mantine/core';
import { GearSixIcon, MagnifyingGlassIcon, ImageIcon, ChatCircleIcon, TrashIcon, IconArrowsLeftRight } from '@phosphor-icons/react';
function Demo() {
return (
<Menu>
<Menu.Target>
<Button>Toggle menu</Button>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Application</Menu.Label>
<Menu.Item leftSection={<GearSixIcon size={16} />}>Settings</Menu.Item>
<Menu.Item leftSection={<ChatCircleIcon size={16} />}>Messages</Menu.Item>
<Menu.Item leftSection={<ImageIcon size={16} />}>Gallery</Menu.Item>
<Menu.Item leftSection={<MagnifyingGlassIcon size={16} />} rightSection={<Text size="xs">⌘K</Text>}>
Search
</Menu.Item>
<Menu.Divider />
<Menu.Label>Danger zone</Menu.Label>
<Menu.Item leftSection={<IconArrowsLeftRight size={16} />}>Transfer my data</Menu.Item>
<Menu.Item leftSection={<TrashIcon size={16} />} color="red">Delete my account</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}
서브메뉴 (Submenus)
Menu.Sub로 중첩 서브메뉴를 만들 수 있어요.
import { Button, Menu } from '@mantine/core';
function Demo() {
return (
<Menu>
<Menu.Target><Button>Toggle Menu</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Item>Dashboard</Menu.Item>
<Menu.Sub label="Products">
<Menu.Item>All products</Menu.Item>
<Menu.Item>Categories</Menu.Item>
<Menu.Item>Tags</Menu.Item>
<Menu.Item>Attributes</Menu.Item>
<Menu.Item>Shipping classes</Menu.Item>
</Menu.Sub>
<Menu.Item>Customers</Menu.Item>
<Menu.Item>Reports</Menu.Item>
<Menu.Sub label="Orders">
<Menu.Item>Open</Menu.Item>
<Menu.Item>Completed</Menu.Item>
<Menu.Item>Cancelled</Menu.Item>
</Menu.Sub>
<Menu.Sub label="Settings">
<Menu.Item>Profile</Menu.Item>
<Menu.Item>Security</Menu.Item>
<Menu.Item>Notifications</Menu.Item>
</Menu.Sub>
</Menu.Dropdown>
</Menu>
);
}
커서가 타겟에서 드롭다운으로 이동하는 동안 서브메뉴를 열린 상태로 유지하려면 safeAreaPolygon을 사용해요. offset이 타겟과 드롭다운 사이에 더 큰 간격을 만들 때처럼 Floating UI safePolygon 옵션을 조정하려면 객체를 전달해요.
검색 (Search)
Menu.Search는 드롭다운 안에 검색 인풋을 렌더링해요. ArrowUp/ArrowDown이 항목 위로 하이라이트를 이동하는 동안 포커스는 인풋에 유지되고, Enter는 하이라이트된 항목을 트리거해요. 필터링은 사용자가 제어해요. value와 onChange를 전달하고 쿼리를 기반으로 Menu.Item children을 필터링해요. 기본적으로 메뉴 닫힘 전환이 완료된 후 검색 값은 자동으로 지워져요. 열릴 때마다 쿼리를 유지하려면 clearSearchOnClose={false}로 비활성화할 수 있어요.
import { useState } from 'react';
import { Button, Menu, Text } from '@mantine/core';
const data = [
'Dashboard', 'Customers', 'Products', 'Orders', 'Reports',
'Settings', 'Integrations', 'Billing', 'Team members', 'Help center',
];
function Demo() {
const [query, setQuery] = useState('');
const items = data.filter((item) => item.toLowerCase().includes(query.toLowerCase().trim()));
return (
<Menu>
<Menu.Target><Button>Toggle menu</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Search value={query} onChange={(e) => setQuery(e.currentTarget.value)} placeholder="Search items" />
{items.length > 0 ? (
items.map((item) => <Menu.Item key={item}>{item}</Menu.Item>)
) : (
<Menu.Item disabled>Nothing found</Menu.Item>
)}
</Menu.Dropdown>
</Menu>
);
}
같은 접근 방식이 서브메뉴에도 적용돼요. 중첩 항목이 쿼리와 일치할 때 부모를 계속 보이게 하려면, 자식 중 하나라도 쿼리와 일치하면 부모를 필터링된 트리에 포함해요. filterTree 재귀 함수로 이를 처리할 수 있어요 (자세한 예시는 원문 참고).
체크박스 항목 (Checkbox items)
Menu.CheckboxItem은 체크 표시기가 있는 메뉴 항목을 렌더링해요. 일반 Checkbox처럼 동작해요. checked/onChange로 상태를 관리하거나 defaultChecked로 비제어 값을 사용할 수 있어요. 기본적으로 체크박스 항목을 클릭해도 메뉴는 닫히지 않아요. 항목에 closeMenuOnClick을 설정하거나(또는 Menu에 closeOnItemClick={false}) 이를 재정의할 수 있어요.
import { useState } from 'react';
import { Button, Menu } from '@mantine/core';
function Demo() {
const [columns, setColumns] = useState({ name: true, email: true, role: false, lastSeen: false });
const setColumn = (key: keyof typeof columns) => (checked: boolean) =>
setColumns((current) => ({ ...current, [key]: checked }));
return (
<Menu>
<Menu.Target><Button>Columns</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Label>Visible columns</Menu.Label>
<Menu.CheckboxItem checked={columns.name} onChange={setColumn('name')}>Name</Menu.CheckboxItem>
<Menu.CheckboxItem checked={columns.email} onChange={setColumn('email')}>Email</Menu.CheckboxItem>
<Menu.CheckboxItem checked={columns.role} onChange={setColumn('role')}>Role</Menu.CheckboxItem>
<Menu.CheckboxItem checked={columns.lastSeen} onChange={setColumn('lastSeen')}>Last seen</Menu.CheckboxItem>
</Menu.Dropdown>
</Menu>
);
}
체크박스 그룹 (Checkbox group)
Menu.CheckboxItem 컴포넌트를 Menu.CheckboxGroup으로 감싸면 단일 value: string[]/onChange 쌍(또는 비제어 defaultValue)으로 다중 선택 상태를 관리할 수 있어요. 각 항목에는 value가 필요해요. 항목을 클릭하면 그룹에서 값이 토글돼요.
import { useState } from 'react';
import { Button, Menu } from '@mantine/core';
function Demo() {
const [columns, setColumns] = useState(['name', 'email']);
return (
<Menu>
<Menu.Target><Button>Columns</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Label>Visible columns</Menu.Label>
<Menu.CheckboxGroup value={columns} onChange={setColumns}>
<Menu.CheckboxItem value="name">Name</Menu.CheckboxItem>
<Menu.CheckboxItem value="email">Email</Menu.CheckboxItem>
<Menu.CheckboxItem value="role">Role</Menu.CheckboxItem>
<Menu.CheckboxItem value="lastSeen">Last seen</Menu.CheckboxItem>
</Menu.CheckboxGroup>
</Menu.Dropdown>
</Menu>
);
}
Menu.CheckboxItem은 자체 checked/defaultChecked/onChange prop으로 그룹 없이도 단독 사용할 수 있어요. 항목 수준의 checked와 onChange는 둘 다 있을 때 그룹을 재정의해요.
라디오 항목 (Radio items)
Menu.RadioItem은 Menu.RadioGroup 안의 단일 옵션을 나타내요. 그룹은 value/onChange(또는 비제어 defaultValue)로 선택된 값을 관리해요. 현재 선택된 항목은 표시기 점을 표시해요. 체크박스 항목처럼 라디오 항목은 기본적으로 클릭해도 메뉴가 닫히지 않아요.
import { useState } from 'react';
import { Button, Menu } from '@mantine/core';
function Demo() {
const [sort, setSort] = useState('newest');
return (
<Menu>
<Menu.Target><Button>Sort by</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Label>Order</Menu.Label>
<Menu.RadioGroup value={sort} onChange={setSort}>
<Menu.RadioItem value="newest">Newest first</Menu.RadioItem>
<Menu.RadioItem value="oldest">Oldest first</Menu.RadioItem>
<Menu.RadioItem value="popular">Most popular</Menu.RadioItem>
<Menu.RadioItem value="commented">Most commented</Menu.RadioItem>
</Menu.RadioGroup>
</Menu.Dropdown>
</Menu>
);
}
표시기가 있는/없는 항목 라벨 정렬 (Aligning labels of items with and without indicators)
Menu의 alignItemsLabels prop으로 표시기 슬롯 공간을 어떻게 확보할지 제어할 수 있어요. Menu.Item을 Menu.CheckboxItem이나 Menu.RadioItem과 섞고 라벨이 같은 가로 위치에서 시작하길 원할 때 유용해요.
alignItemsLabels="with-indicators"(기본값) –Menu.CheckboxItem과Menu.RadioItem에만 표시기 공간을 확보해요. 일반Menu.Item은 패딩되지 않아요.alignItemsLabels="all"– 모든Menu.Item에 표시기 공간을 확보해서 일반 항목의 라벨이 체크박스·라디오 항목과 정렬돼요.alignItemsLabels="none"– 현재 표시기를 보여주는 항목에만 표시기 공간을 확보해요. 체크되지 않은 체크박스·라디오 항목은 슬롯 없이 렌더링돼요 (토글할 때 레이아웃이 이동해요).
커스텀 체크 아이콘 (Custom check icon)
checkIcon prop으로 Menu.CheckboxItem과 Menu.RadioItem이 렌더링하는 기본 표시기를 바꿀 수 있어요. Menu에 checkIcon을 설정하면 드롭다운의 모든 체크박스/라디오 항목에 적용돼요. 개별 항목에 checkIcon을 설정하면 메뉴 수준 값을 재정의해요.
import { useState } from 'react';
import { CheckIcon } from '@phosphor-icons/react';
import { Button, Menu } from '@mantine/core';
function Demo() {
const [filters, setFilters] = useState({ open: true, drafts: false, archived: false });
const setFilter = (key: keyof typeof filters) => (checked: boolean) =>
setFilters((current) => ({ ...current, [key]: checked }));
return (
<Menu checkIcon={<CheckIcon size={16} />}>
<Menu.Target><Button>Filters</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Label>Filters</Menu.Label>
<Menu.CheckboxItem checked={filters.open} onChange={setFilter('open')}>Open</Menu.CheckboxItem>
<Menu.CheckboxItem checked={filters.drafts} onChange={setFilter('drafts')}>Drafts</Menu.CheckboxItem>
<Menu.CheckboxItem checked={filters.archived} onChange={setFilter('archived')}>Archived</Menu.CheckboxItem>
</Menu.Dropdown>
</Menu>
);
}
타입어헤드 네비게이션 (Type-ahead navigation)
포커스가 드롭다운 안에 있고 Menu.Search를 사용하지 않을 때, 인쇄 가능한 문자 키를 누르면 입력한 문자로 시작하는 다음 메뉴 항목으로 포커스가 이동해요. 같은 문자를 다시 누르면 그 문자로 시작하는 항목을 순환해요. 빠르게 연속(500ms 이내) 입력한 여러 문자는 전체 입력 문자열로 시작하는 항목과 일치해요. 비활성 항목은 건너뛰어요.
컨텍스트 메뉴 (Context menu)
Menu.ContextMenu를 사용하면 우클릭 시 커서 위치에 메뉴 드롭다운을 열 수 있어요. Menu.Target을 대체하고 contextmenu 이벤트에 응답해야 할 요소를 감싸요. 브라우저의 기본 컨텍스트 메뉴는 억제되고 Mantine Menu.Dropdown이 커서 위치에 배치돼요. 다시 우클릭하면 드롭다운이 새 좌표로 재배치돼요. disabled를 설정하면 브라우저 기본 컨텍스트 메뉴를 복원할 수 있어요.
import { Menu, Paper, Text } from '@mantine/core';
function Demo() {
return (
<Paper withBorder radius="md" p="xl" maw={400} mx="auto">
<Menu.ContextMenu>
<Menu.Target>
<div>
<Text>Right-click anywhere inside this area</Text>
<Text size="sm" c="dimmed">The menu will open at the cursor position</Text>
</div>
</Menu.Target>
<Menu.Dropdown>
<Menu.Label>Actions</Menu.Label>
<Menu.Item>Open</Menu.Item>
<Menu.Item>Rename</Menu.Item>
<Menu.Item>Duplicate</Menu.Item>
<Menu.Divider />
<Menu.Item color="red">Delete</Menu.Item>
</Menu.Dropdown>
</Menu.ContextMenu>
</Paper>
);
}
터치 기기 (Touch devices)
contextmenu 이벤트를 발생시키지 않는 터치 기기(특히 iOS Safari)에서는 길게 누르기로 드롭다운이 열려요. longPressDelay prop으로 드롭다운이 열리기 전 요소를 눌러야 하는 시간을 제어할 수 있고, 기본값은 500ms예요.
import { Menu } from '@mantine/core';
function Demo() {
return (
<Menu.ContextMenu longPressDelay={500}>
<Menu.Target><div>Long-press me</div></Menu.Target>
<Menu.Dropdown>
<Menu.Item>Copy</Menu.Item>
<Menu.Item>Paste</Menu.Item>
</Menu.Dropdown>
</Menu.ContextMenu>
);
}
터치 기기에서 드롭다운 아래에 네이티브 텍스트 선택 콜아웃이 나타나는 것을 막기 위해, Menu.ContextMenu은 감싼 요소에서 텍스트 선택(user-select: none)을 비활성화해요.
제어 방식 (Controlled)
드롭다운의 열림 상태는 opened와 onChange prop으로 제어할 수 있어요.
import { useState } from 'react';
import { Menu } from '@mantine/core';
function Demo() {
const [opened, setOpened] = useState(false);
return <Menu opened={opened} onChange={setOpened}>{/* Menu content */}</Menu>;
}
hover 시 메뉴 표시 (Show menu on hover)
trigger="hover"로 설정하면 메뉴 타겟과 드롭다운 위에 hover할 때 드롭다운이 나타나요. closeDelay와 openDelay prop으로 열림/닫힘 지연(ms)을 제어할 수 있어요. 참고:
closeDelay={0}으로 설정하면 사용자가 드롭다운에 도달하기 전에 메뉴가 닫히므로, 타겟 요소와 드롭다운 사이의 공간을 없애려면offset={0}을 설정해요.trigger="hover"메뉴는 접근 불가능해요. 키보드로 탐색하는 사용자는 사용할 수 없어요. hover와 클릭 트리거가 모두 필요하면trigger="click-hover"를 사용해요.
모든 기기에서 접근 가능한 hover 메뉴를 만들려면 trigger="click-hover"를 사용해요. 데스크톱에서는 hover, 모바일 기기에서는 클릭으로 드롭다운이 열려요.
비활성 항목 (Disabled items)
import { Menu, Button } from '@mantine/core';
import { MagnifyingGlassIcon } from '@phosphor-icons/react';
function Demo() {
return (
<Menu>
<Menu.Target><Button>Toggle menu</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<MagnifyingGlassIcon size={16} />} disabled>Search</Menu.Item>
{/* Other items ... */}
</Menu.Dropdown>
</Menu>
);
}
드롭다운 위치 (Dropdown position)
드롭다운 위치는 position, offset, withArrow, arrowPosition 등의 prop으로 제어할 수 있어요.
import { Menu } from '@mantine/core';
function Demo() {
return (
<Menu position="bottom" offset={8} withArrow>
{/* Menu items */}
</Menu>
);
}
전환 (Transitions)
Menu 드롭다운은 Transition 컴포넌트의 사전 제작 전환 중 하나로 애니메이션 처리할 수 있어요.
import { Menu } from '@mantine/core';
function Demo() {
return (
<Menu transitionProps={{ transition: 'rotate-left', duration: 150 }}>
{/* Menu content */}
</Menu>
);
}
Menu.Item으로 커스텀 컴포넌트 (Custom component as Menu.Item)
기본적으로 Menu.Item은 button 요소로 렌더링돼요. 변경하려면 component prop을 설정해요.
import { Menu, Button } from '@mantine/core';
import { ArrowSquareOutIcon } from '@phosphor-icons/react';
function Demo() {
return (
<Menu>
<Menu.Target><Button>Toggle menu</Button></Menu.Target>
<Menu.Dropdown>
<Menu.Item leftSection={<ArrowSquareOutIcon size={16} />}>Mantine website</Menu.Item>
<Menu.Item component="a" href="https://mantine.dev" target="_blank">External link</Menu.Item>
</Menu.Dropdown>
</Menu>
);
}
component prop에 전달하는 컴포넌트는 루트 요소에 prop을 스프레드할 수 있어야 해요.
타겟으로 커스텀 컴포넌트 (Custom component as target)
Menu.Target 안에는 React 컴포넌트를 사용할 수 있어요. 예를 들어 아바타와 사용자 정보를 가진 버튼을 만들 수 있어요 (자세한 예시는 원문 참고).
Styles API
Menu은 Styles API를 지원해요. classNames prop으로 컴포넌트의 내부 요소에 스타일을 추가할 수 있어요.
Styles API 셀렉터:
dropdown– 드롭다운 요소arrow– 드롭다운 화살표overlay– 오버레이 요소divider–Menu.Divider루트 요소label–Menu.Label루트 요소item–Menu.Item루트 요소itemLabel–Menu.Item의 라벨itemSection–Menu.Item의 왼쪽·오른쪽 섹션itemIndicator–Menu.CheckboxItem과Menu.RadioItem의 표시기 슬롯chevron– 서브 메뉴 치브론search–Menu.Search인풋 요소
Menu.Target children
Menu.Target은 단일 자식으로 요소나 컴포넌트를 요구해요. 문자열, 프래그먼트, 숫자, 여러 요소/컴포넌트는 지원되지 않고 오류를 던져요. 커스텀 컴포넌트는 루트 요소 ref를 얻는 prop을 제공해야 해요. 모든 Mantine 컴포넌트는 기본적으로 ref를 지원해요.
import { Menu, Button } from '@mantine/core';
function Demo() {
return (
<>
<Menu.Target><button>Native button – ok</button></Menu.Target>
{/* OK */}
<Menu.Target><Button>Mantine component – ok</Button></Menu.Target>
{/* String, NOT OK – will throw error */}
{/* {`Raw string`} */}
{/* Number, NOT OK – will throw error */}
{/* {2} */}
{/* Fragment, NOT OK – will throw error */}
{/* <><span>Fragment, NOT OK, will throw error</span></> */}
{/* Multiple nodes, NOT OK – will throw error */}
{/* <>More that one node</> */}
</>
);
}
필수 ref prop (Required ref prop)
Menu.Target 안에서 렌더링되는 커스텀 컴포넌트는 ref prop을 지원해야 해요. ref를 루트 요소에 전달하지 않으면 동작하지 않아요.
// Example of code that WILL NOT WORK
import { Menu } from '@mantine/core';
// ❌ ref is not forwarded to the root element
function MyComponent() {
return <div>My component</div>;
}
ref를 루트 요소에 전달하면 동작해요.
// Example of code that will work
import { Menu } from '@mantine/core';
// ✅ ref is forwarded to the root element
function MyComponent({ ref, ...others }: React.ComponentProps<'div'>) {
return <div {...others} ref={ref}>My component</div>;
}
접근성 (Accessibility)
Menu는 WAI-ARIA 권장사항을 따릅니다.
- 드롭다운 요소에는
role="menu"와aria-labelledby="target-id"속성이 있어요 - 타겟 요소에는
aria-haspopup="menu",aria-expanded,aria-controls="dropdown-id"속성이 있어요 - 메뉴 항목에는
role="menuitem"속성이 있어요
드롭다운이 닫혀 있는 동안 aria-controls 속성은 undefined가 돼요.
지원 타겟 요소 (Supported target elements)
trigger="click"(기본값)인 비제어 Menu는 button 요소나 그것을 렌더링하는 컴포넌트(Button, ActionIcon 등)와 함께 쓸 때만 접근 가능해요. 다른 요소는 Space와 Enter 키 입력을 지원하지 않아요.
hover 메뉴 (Hover menu)
trigger="hover" 메뉴는 접근 불가능해요. 키보드로 접근할 수 없어요. 접근성이 중요하지 않을 때만 사용해요. hover와 클릭 트리거가 모두 필요하면 trigger="click-hover"를 사용해요.
네비게이션 (Navigation)
네비게이션을 만들기 위해 Menu를 사용한다면, 아래 데모의 옵션을 사용해 WAI-ARIA 네비게이션 권장사항을 따를 수 있어요.
키보드 상호작용 (Keyboard interactions)
| Key | Description | Condition |
|---|---|---|
| Escape | 드롭다운 닫기 | 드롭다운 내부 포커스 |
| Space/Enter | 드롭다운 열기/닫기 | 타겟 요소 포커스 |
| ArrowUp | 이전 메뉴 항목으로 포커스 이동 | 드롭다운 내부 포커스 |
| ArrowDown | 다음 메뉴 항목으로 포커스 이동 | 드롭다운 내부 포커스 |
| Home | 첫 메뉴 항목으로 포커스 이동 | 드롭다운 내부 포커스 |
| End | 마지막 메뉴 항목으로 포커스 이동 | 드롭다운 내부 포커스 |
| ArrowUp/ArrowDown | 인풋을 떠나지 않고 이전/다음 메뉴 항목으로 하이라이트 이동 | Menu.Search 포커스 |
| Enter | 하이라이트된 항목 트리거 | Menu.Search 포커스 |
| Printable character | 입력한 문자로 시작하는 다음 항목으로 포커스 이동. 같은 문자를 다시 누르면 일치 항목을 순환. 500ms 내 연속 입력한 여러 문자는 입력 문자열로 시작하는 항목과 일치 | 드롭다운 내부 포커스, Menu.Search 없음 |
Tab과 Shift + Tab도 지원해야 한다면 menuItemTabIndex={0}을 설정해요.