@leafygreen-ui/menu
v33.1.0
Published
LeafyGreen UI Kit Menu
Downloads
241,399
Keywords
Readme
Menu
View on MongoDB.design
Installation
PNPM
pnpm add @leafygreen-ui/menuYarn
yarn add @leafygreen-ui/menuNPM
npm install @leafygreen-ui/menuPeer Dependencies
| Package | Version |
| ------------------------------------ | -------- |
| @leafygreen-ui/leafygreen-provider | ^1.1.0 |
Example
Basic Usage
import { Menu, MenuItem } from '@leafygreen-ui/menu';
function BasicExample() {
const [open, setOpen] = useState(false);
return (
<Menu trigger={<Button>Open Menu</Button>} open={open} setOpen={setOpen}>
<MenuItem>Option 1</MenuItem>
<MenuItem>Option 2</MenuItem>
<MenuItem disabled>Disabled Option</MenuItem>
</Menu>
);
}With Descriptions and Icons
import { Menu, MenuItem } from '@leafygreen-ui/menu';
import Icon from '@leafygreen-ui/icon';
<Menu trigger={<Button>Actions</Button>}>
<MenuItem
glyph={<Icon glyph="Edit" />}
description="Make changes to this item"
>
Edit
</MenuItem>
<MenuItem glyph={<Icon glyph="Copy" />} description="Create a duplicate">
Duplicate
</MenuItem>
<MenuItem
variant="destructive"
glyph={<Icon glyph="Trash" />}
description="Permanently delete this item"
>
Delete
</MenuItem>
</Menu>;With SubMenus and Groups
import {
Menu,
MenuItem,
SubMenu,
MenuGroup,
MenuSeparator,
} from '@leafygreen-ui/menu';
import Icon from '@leafygreen-ui/icon';
<Menu trigger={<Button>File</Button>}>
<MenuItem glyph={<Icon glyph="Plus" />}>New</MenuItem>
<MenuItem glyph={<Icon glyph="ImportantWithCircle" />}>Open</MenuItem>
<MenuSeparator />
<SubMenu title="Recent Files" glyph={<Icon glyph="Clock" />}>
<MenuItem>Document1.pdf</MenuItem>
<MenuItem>Spreadsheet.xlsx</MenuItem>
<MenuItem>Presentation.pptx</MenuItem>
</SubMenu>
<MenuSeparator />
<MenuGroup title="Export Options" glyph={<Icon glyph="Export" />}>
<MenuItem>Export as PDF</MenuItem>
<MenuItem>Export as CSV</MenuItem>
<SubMenu title="Image Formats">
<MenuItem>PNG</MenuItem>
<MenuItem>JPEG</MenuItem>
<MenuItem>SVG</MenuItem>
</SubMenu>
</MenuGroup>
</Menu>;Controlled vs Uncontrolled
// Controlled Menu
function ControlledExample() {
const [isOpen, setIsOpen] = useState(false);
return (
<Menu
trigger={<Button>Controlled Menu</Button>}
open={isOpen}
setOpen={setIsOpen}
>
<MenuItem onClick={() => setIsOpen(false)}>Close Menu</MenuItem>
</Menu>
);
}
// Uncontrolled Menu with initial state
<Menu trigger={<Button>Uncontrolled Menu</Button>} initialOpen={false}>
<MenuItem>Item 1</MenuItem>
<MenuItem>Item 2</MenuItem>
</Menu>;Custom Trigger Function
<Menu
trigger={({ onClick, children }) => (
<Button onClick={onClick} rightGlyph={<Icon glyph="CaretDown" />}>
Custom Trigger
{children}
</Button>
)}
>
<MenuItem>Option A</MenuItem>
<MenuItem>Option B</MenuItem>
</Menu>With FocusableMenuItem
Use FocusableMenuItem when you need to include interactive elements like inputs within menu items:
import { Menu, MenuItem, FocusableMenuItem } from '@leafygreen-ui/menu';
import TextInput from '@leafygreen-ui/text-input';
<Menu trigger={<Button>Search Menu</Button>}>
<FocusableMenuItem>
<TextInput placeholder="Search..." aria-label="Search" />
</FocusableMenuItem>
<MenuSeparator />
<MenuItem>Recent Search 1</MenuItem>
<MenuItem>Recent Search 2</MenuItem>
</Menu>;Compact Variant
<Menu
variant={MenuVariant.Compact}
trigger={<Button size="small">Compact Menu</Button>}
>
<MenuItem>Compact Item 1</MenuItem>
<MenuItem description="This description will be hidden">
Compact Item 2
</MenuItem>
<MenuItem>Compact Item 3</MenuItem>
</Menu>Usage with NextJS Link components
We recommend using Menu with NextJS's link components in the following pattern:
import NextLink from 'next/link';
function CustomLink({ href, children, ...props }) {
return (
<NextLink href={href}>
<a {...props}>{children}</a>
</NextLink>
);
}
<Menu trigger={<Button>Navigation</Button>}>
<MenuItem as={CustomLink} href="/dashboard">
Dashboard
</MenuItem>
<SubMenu title="Settings">
<MenuItem as={CustomLink} href="/settings/profile">
Profile
</MenuItem>
<MenuItem as={CustomLink} href="/settings/security">
Security
</MenuItem>
</SubMenu>
</Menu>;This pattern is recommended because the SubMenu component expects to pass styling through the className prop, which would not apply correctly if it was passed to NextLink.
Accessibility
The Menu component includes comprehensive keyboard navigation and accessibility features:
Keyboard Navigation
- Arrow Keys: Navigate between menu items
↓(Down): Move to next menu item, or first item if at the end↑(Up): Move to previous menu item, or last item if at the beginning→(Right): Open SubMenu or move to first item in SubMenu←(Left): Close SubMenu or move to parent menu
- Enter/Space: Activate the focused menu item
- Escape: Close the menu
- Tab: Close the menu and move focus to the next focusable element
ARIA Support
- Menu elements have appropriate
role="menu"androle="menuitem"attributes - SubMenus include
aria-expandedandaria-haspopupattributes - Active items are marked with
aria-current="true" - Disabled items have
aria-disabled="true" - Menu groups are properly labeled with
aria-labelledby
API
Menu
Properties
| Prop | Type | Description | Default |
| ------------------ | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------- | ------------- |
| children | ReactNode | The menu items, submenus, groups, and separators | |
| trigger | ReactElement | function | Element or function that triggers the menu. Function signature: ({ onClick, children }) => ReactElement | |
| open | boolean | Controls whether the menu is open or closed | false |
| setOpen | function | Callback to control the open state: (open: boolean) => void | |
| initialOpen | boolean | Initial open state for uncontrolled usage | false |
| shouldClose | function | Callback to determine if menu should close: () => boolean | () => true |
| onOpen | function | Callback fired when menu opens (after transition) | |
| onClose | function | Callback fired when menu closes (after transition) | |
| align | 'top' | 'bottom' | 'left' | 'right' | Alignment of the menu relative to its trigger | 'bottom' |
| justify | 'start' | 'middle' | 'end' | Justification of the menu relative to its trigger | 'end' |
| maxHeight | number | Maximum height of the menu in pixels | 256 |
| darkMode | boolean | Determines if the component renders in dark mode | false |
| renderDarkMenu | boolean | Whether the menu should always render dark, regardless of theme context | true |
| variant | 'default' | 'compact' | Visual variant of the menu. Compact hides descriptions | 'default' |
| refEl | HTMLElement | Reference element for positioning (alternative to trigger) | |
| adjustOnMutation | boolean | Whether menu should reposition on DOM mutations | false |
| renderMode | 'inline' | 'portal' | 'top-layer' | Rendering mode. Use 'top-layer' for modern browsers | 'top-layer' |
| id | string | ID for the menu dropdown element | |
| data-testid | string | Test ID for the menu element | |
| ... | native ul attributes | Any other props are spread to the root ul element | |
Note: Some props like portalContainer, scrollContainer, portalClassName, and popoverZIndex are deprecated. Use renderMode="top-layer" instead.
MenuItem
Properties
| Prop | Type | Description | Default |
| ------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------- |
| children | ReactNode | Content to appear inside of the MenuItem | |
| href | string | If supplied, renders the MenuItem as an <a> tag instead of a <button> | |
| as | React.ElementType | Determines what element the MenuItem renders as (e.g., 'a', 'button', custom component) | 'button' |
| onClick | function | Callback fired when the MenuItem is clicked | |
| active | boolean | Whether the MenuItem appears in an active state | false |
| disabled | boolean | Whether the MenuItem is disabled and non-interactive | false |
| description | ReactNode | Additional content displayed below the main text (hidden in compact variant) | |
| glyph | React.ReactElement | Icon displayed to the left of the MenuItem text | |
| rightGlyph | React.ReactElement | Icon displayed to the right of the MenuItem text | |
| variant | 'default' | 'destructive' | Visual variant of the MenuItem | 'default' |
| className | string | CSS class applied to the root <li> element | |
| ... | native attributes of component passed to as prop | Any other props are spread to the rendered element | |
SubMenu
SubMenu allows you to create nested menu structures with expandable sections.
Properties
| Prop | Type | Description | Default |
| ------------- | -------------------------------------- | --------------------------------------------------------------------- | ---------- |
| children | ReactNode | Menu items, submenus, groups, or separators to display in the submenu | |
| title | string | Text displayed in the submenu trigger | |
| open | boolean | Controls whether the submenu is expanded | false |
| setOpen | function | Callback to control the open state: (open: boolean) => void | |
| initialOpen | boolean | Initial open state for uncontrolled usage | false |
| active | boolean | Whether the SubMenu trigger appears active | false |
| disabled | boolean | Whether the SubMenu is disabled and non-interactive | false |
| description | ReactNode | Additional content displayed below the title | |
| glyph | React.ReactElement | Icon displayed to the left of the SubMenu title | |
| href | string | If supplied, renders the SubMenu trigger as an <a> tag | |
| as | React.ElementType | Determines what element the SubMenu trigger renders as | 'button' |
| onEntered | function | Callback fired when the submenu finishes opening | |
| onExited | function | Callback fired when the submenu finishes closing | |
| className | string | CSS class applied to the root element | |
| ... | native anchor or button attributes | Any other props are spread to the trigger element | |
MenuGroup
MenuGroup provides visual grouping and labeling for related menu items.
Properties
| Prop | Type | Description | Default |
| ----------- | ----------------------- | ----------------------------------------------------------- | ------- |
| children | ReactNode | Menu items, submenus, or separators to display in the group | |
| title | string | Label displayed at the top of the group | |
| glyph | React.ReactElement | Icon displayed to the left of the group title | |
| className | string | CSS class applied to the root element | |
| ... | native div attributes | Any other props are spread to the root <div> element | |
MenuSeparator
MenuSeparator provides visual separation between menu items or groups.
Properties
MenuSeparator accepts no props and renders a horizontal divider line.
import { MenuSeparator } from '@leafygreen-ui/menu';
<MenuSeparator />;FocusableMenuItem
FocusableMenuItem allows you to include interactive elements (like inputs) within menu items while maintaining proper keyboard navigation.
Properties
| Prop | Type | Description | Default |
| ----------- | ---------------------- | ------------------------------------------------------------- | ------- |
| children | ReactNode | Interactive content to display within the focusable menu item | |
| className | string | CSS class applied to the root element | |
| ... | native li attributes | Any other props are spread to the root <li> element | |
Utilities
getLgIds
The getLgIds utility function generates consistent data-lgid values for menu components, useful for testing and automation.
import { getLgIds } from '@leafygreen-ui/menu';
const lgIds = getLgIds('my-menu');
// Returns: {
// root: 'my-menu',
// item: 'my-menu-menu_item',
// submenu: 'my-menu-submenu',
// submenuToggle: 'my-menu-submenu_toggle'
// }
<Menu data-lgid={lgIds.root}>
<MenuItem data-lgid={lgIds.item}>Item</MenuItem>
</Menu>;Advanced Usage
Controlling Menu State
function AdvancedMenuExample() {
const [isOpen, setIsOpen] = useState(false);
const handleClose = () => {
console.log('Menu closing');
setIsOpen(false);
};
const shouldPreventClose = () => {
// Only close if form is valid
return isFormValid;
};
return (
<Menu
open={isOpen}
setOpen={setIsOpen}
onClose={handleClose}
shouldClose={shouldPreventClose}
trigger={<Button>Advanced Menu</Button>}
>
<MenuItem onClick={() => console.log('Action performed')}>
Action Item
</MenuItem>
</Menu>
);
}Menu Positioning
// Position relative to a custom element
function PositionedMenu() {
const triggerRef = useRef(null);
return (
<>
<div ref={triggerRef}>Custom Trigger Element</div>
<Menu refEl={triggerRef.current} align="bottom" justify="start">
<MenuItem>Positioned Item</MenuItem>
</Menu>
</>
);
}Dynamic Menu Content
function DynamicMenu({ items, onItemClick }) {
return (
<Menu trigger={<Button>Dynamic Menu</Button>}>
{items.map(item => (
<MenuItem
key={item.id}
onClick={() => onItemClick(item)}
disabled={item.disabled}
variant={item.destructive ? 'destructive' : 'default'}
glyph={item.icon}
>
{item.label}
</MenuItem>
))}
</Menu>
);
}Migration Notes
From v31 to v32
- The
sizeprop onMenuItemhas been deprecated and no longer has any effect - Use
variant="compact"on theMenucomponent instead for smaller menu items - The deprecated portal-related props will be removed in a future version - migrate to
renderMode="top-layer"
Best Practices
- Accessibility: Always provide meaningful labels and ensure keyboard navigation works
- Performance: Use
shouldClosecallback to prevent unnecessary re-renders - Positioning: Use
renderMode="top-layer"for better stacking context management - Testing: Use the
getLgIdsutility for consistent test selectors
Browser Support
The Menu component supports all modern browsers. The renderMode="top-layer" option requires browsers that support the HTML dialog element and CSS @layer. For older browsers, the component will automatically fall back to portal rendering.
