Skip to content

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.
PropertyTypeDescription
groupsMenuGroup[]Array of menu groups.
openOnHoverbooleanWhether to open the menu on hover.
PropertyTypeDescription
idstringUnique group identifier.
labelstringOptional label displayed above the group.
itemsMenuItem[]Array of menu items.
itemSelectionType'radio' | 'checkbox' | 'none'Selection behavior for items in this group.
closeOnItemClickbooleanWhether to close the menu on item click.
PropertyTypeDescription
idstringUnique item identifier.
labelstringDisplay text.
onClick(id: string, event: MouseEvent) => voidClick handler.
iconReactNodeOptional icon before the label.
disabledbooleanWhether the item is disabled.
subMenuMenuConfigNested submenu configuration.
submenuTrigger'hover' | 'click'How the submenu opens.

Accessibility

  • Built on @base-ui/react Menu primitives with WAI-ARIA menu pattern
  • Full keyboard navigation: Arrow Up/Down to move, Enter to activate, Escape to close
  • Radio groups use BaseMenu.RadioGroup with proper role semantics
  • Group labels are rendered via BaseMenu.GroupLabel for screen readers
  • Disabled items are properly excluded from keyboard navigation
  • Focus is returned to the trigger when the menu closes