better-mui-menu
v1.5.2
Published
MUI Menu enhancement for deeply nested menus with keyboard navigation and accessibility in mind.
Readme
better-mui-menu
better-mui-menu is a zero-dependency wrapper for Material UI Menu to support nested menus and full keyboard accessibility with proper focus management.
Live demo

Features
- Excellent Keyboard Navigation for Accessibility – navigate with arrow keys, open submenus with right arrow or Enter, close them with left arrow or Escape, and have the menu stack stay in sync with focus.
- Unlimited nesting – describe every submenu with an
itemsarray andbetter-mui-menurendersNestedMenuItempoppers that stay synchronized with their parents. - Customizable – style your menu and menu items using MUI's Menu and MenuItem props.
- Data-driven API – you keep work in a single
MenuItem[]list; leaves, dividers, and nested branches all live alongside each other.
Installation
(yarn | npm | pnpm) install better-mui-menuSince the component renders Material UI primitives, also install the peer dependencies:
npm install @mui/material @emotion/react @emotion/styled @mui/icons-materialUsage
import { Menu, type MenuItem } from 'better-mui-menu'
const menuItems: MenuItem[] = [
{
id: 'save',
label: 'Save',
startIcon: Save,
onClick: () => console.log('Save action'),
},
{ type: 'divider' },
{
label: (
<Stack>
Cloud actions
<Typography variant="caption" color="text.secondary">
Requires internet connection
</Typography>
</Stack>
),
startIcon: <Cloud fontSize='small' sx={{ ml: 0.5 }} />,
items: [
{ label: 'Upload', onClick: () => console.log('Upload') },
{ label: 'Download', onClick: () => console.log('Download') },
],
},
]
export function FileMenu() {
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
return (
<>
<Button variant="contained" onClick={event => setAnchorEl(event.currentTarget)}>
Open file menu
</Button>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={() => setAnchorEl(null)}
items={menuItems}
/>
</>
)
}MenuItem Reference
MenuItem extends @mui/material/MenuItemProps (excluding children) so you can still pass disabled, sx, aria-*, data-*, etc.
| Field | Applies to | Type / values | Required | Default | Notes |
| --- | --- | --- | --- | --- | --- |
| type | all entries | 'item' \| 'divider' \| 'header' | no | 'item' | Use 'divider' for separators, 'header' for section labels. |
| id | 'item' | string | no | auto-generated | Used for stable ARIA/menu item IDs. |
| label | 'item', 'header' | text or JSX | yes ('item', 'header') | none | Not used for 'divider'. |
| startIcon | 'item' | MUI Icon or JSX | no | none | Example: Save or <Save fontSize='small' />. |
| endIcon | 'item' | MUI Icon or JSX | no | none | For submenu triggers, a right-arrow icon is shown when omitted. |
| items | 'item' | MenuItem[] | no | none | If present and non-empty, renders a nested submenu. |
| onClick | 'item' | function | no | none | Runs on leaf item click; menu stack closes afterward. |
| ...MenuItemProps | 'item' | MUI MenuItem props (except children, type) | no | MUI defaults | Example: disabled, dense, selected, data-*. |
