Reflejo/better-mui-menu

MUI Menu enhancement for deeply nested menus with keyboard navigation and accessibility in mind.

★ 0Forks 0TypeScriptGitHub ↗Compare

Project website ↗

README

better-mui-menu

CI Security coverage npm version npm downloads License: MIT

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

Edit better-mui-menu-demo

Edit in StackBlitz

Menu demo preview

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 items array and better-mui-menu renders NestedMenuItem poppers 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-menu

Since the component renders Material UI primitives, also install the peer dependencies:

npm install @mui/material @emotion/react @emotion/styled @mui/icons-material

Usage

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-*.

Contributors

eggeiReflejo

Issues