Menu
Configuration-driven menu component for editor interfaces. Automatically handles radio and checkbox selection states, item grouping with visual separators, and nested submenus. Built on top of @base-ui/react Menu primitives with full keyboard navigation support.
Live Preview
Import
import { Menu } from 'entangle-ui';Usage
const config = { groups: [ { id: 'actions', items: [ { id: 'copy', label: 'Copy', onClick: handleCopy }, { id: 'paste', label: 'Paste', onClick: handlePaste }, { id: 'delete', label: 'Delete', onClick: handleDelete }, ], itemSelectionType: 'none', }, ],};
<Menu config={config}> <span>Options</span></Menu>;The children element becomes the menu trigger button.
Selection Types
Each group can have a different selection behavior.
No Selection
Items act as simple click handlers with no selection state.
const config = { groups: [ { id: 'actions', items: [ { id: 'save', label: 'Save', onClick: handleSave }, { id: 'export', label: 'Export', onClick: handleExport }, ], itemSelectionType: 'none', }, ],};Radio Selection
Single selection within a group. Only one item can be active at a time.
const [selected, setSelected] = useState({ viewMode: ['perspective'] });
const config = { groups: [ { id: 'viewMode', label: 'View Mode', items: [ { id: 'perspective', label: 'Perspective', onClick: () => {} }, { id: 'orthographic', label: 'Orthographic', onClick: () => {} }, { id: 'front', label: 'Front', onClick: () => {} }, ], itemSelectionType: 'radio', }, ],};
<Menu config={config} selectedItems={selected} onChange={setSelected}> <span>View</span></Menu>;Checkbox Selection
Multiple items can be selected within a group.
const [selected, setSelected] = useState({ overlays: ['grid', 'wireframe'],});
const config = { groups: [ { id: 'overlays', label: 'Overlays', items: [ { id: 'grid', label: 'Grid', onClick: () => {} }, { id: 'wireframe', label: 'Wireframe', onClick: () => {} }, { id: 'normals', label: 'Normals', onClick: () => {} }, ], itemSelectionType: 'checkbox', }, ],};
<Menu config={config} selectedItems={selected} onChange={setSelected}> <span>Display</span></Menu>;Multiple Groups
Groups are visually separated with dividers. Different groups can have different selection types.
const config = { groups: [ { id: 'actions', items: [ { id: 'undo', label: 'Undo', onClick: handleUndo }, { id: 'redo', label: 'Redo', onClick: handleRedo }, ], itemSelectionType: 'none', }, { id: 'view', label: 'View Mode', items: [ { id: 'solid', label: 'Solid', onClick: () => {} }, { id: 'wireframe', label: 'Wireframe', onClick: () => {} }, ], itemSelectionType: 'radio', }, ],};Nested Submenus
Items can have a subMenu property containing another MenuConfig. Submenus open on hover by default, or on click with submenuTrigger: 'click'.
const config = { groups: [ { id: 'actions', items: [ { id: 'transform', label: 'Transform', onClick: () => {}, subMenu: { groups: [ { id: 'transforms', items: [ { id: 'move', label: 'Move', onClick: handleMove }, { id: 'rotate', label: 'Rotate', onClick: handleRotate }, { id: 'scale', label: 'Scale', onClick: handleScale }, ], itemSelectionType: 'none', }, ], }, submenuTrigger: 'hover', // default }, ], itemSelectionType: 'none', }, ],};Items with Icons
const config = { groups: [ { id: 'actions', items: [ { id: 'copy', label: 'Copy', icon: <CopyIcon />, onClick: handleCopy }, { id: 'paste', label: 'Paste', icon: <PasteIcon />, onClick: handlePaste, }, ], itemSelectionType: 'none', }, ],};Disabled Items
Individual items can be disabled.
const config = { groups: [ { id: 'actions', items: [ { id: 'copy', label: 'Copy', onClick: handleCopy }, { id: 'paste', label: 'Paste', onClick: handlePaste, disabled: true }, ], itemSelectionType: 'none', }, ],};Custom Selection Icons
Override the default check and radio icons.
<Menu config={config} selectedItems={selected} onChange={setSelected} checkboxIcon={<MyCheckIcon />} radioIcon={<MyRadioIcon />}> <span>Settings</span></Menu>Props
| Prop | Type | Default | Description |
|---|---|---|---|
config | MenuConfig | — | Menu configuration object defining groups and items. |
selectedItems | Record<string, string[]> | — | Currently selected items organized by group ID. |
onChange | (selection: MenuSelection) => void | — | Callback when selection state changes. |
children | ReactNode | — | Menu trigger element rendered inside a Button. |
checkboxIcon | ReactNode | <CheckIcon /> | Custom icon for checkbox selected state. |
radioIcon | ReactNode | <CircleIcon /> | Custom icon for radio selected state. |
disabled | boolean | false | Whether the menu trigger is disabled. |
className | string | — | Additional CSS class names for the menu popup. |
testId | string | — | Test identifier for automated testing. |
MenuConfig
| Property | Type | Description |
|---|---|---|
groups | MenuGroup[] | Array of menu groups. |
openOnHover | boolean | Whether to open the menu on hover. |
MenuGroup
| Property | Type | Description |
|---|---|---|
id | string | Unique group identifier. |
label | string | Optional label displayed above the group. |
items | MenuItem[] | Array of menu items. |
itemSelectionType | 'radio' | 'checkbox' | 'none' | Selection behavior for items in this group. |
closeOnItemClick | boolean | Whether to close the menu on item click. |
MenuItem
| Property | Type | Description |
|---|---|---|
id | string | Unique item identifier. |
label | string | Display text. |
onClick | (id: string, event: MouseEvent) => void | Click handler. |
icon | ReactNode | Optional icon before the label. |
disabled | boolean | Whether the item is disabled. |
subMenu | MenuConfig | Nested submenu configuration. |
submenuTrigger | 'hover' | 'click' | How the submenu opens. |
Accessibility
- Built on
@base-ui/reactMenu primitives with WAI-ARIA menu pattern - Full keyboard navigation: Arrow Up/Down to move, Enter to activate, Escape to close
- Radio groups use
BaseMenu.RadioGroupwith proper role semantics - Group labels are rendered via
BaseMenu.GroupLabelfor screen readers - Disabled items are properly excluded from keyboard navigation
- Focus is returned to the trigger when the menu closes