@edux-design/menus
v1.0.3
Published
Accessible menu primitives built on top of the popover system.
Readme
@edux-design/menus
Accessible menu primitives built on top of the popover system.
The package exports:
Menufor either a trigger button menu or a viewport-anchored context menuMenuListfor the focus-managed menu surfaceMenuGroup,MenuGroupLabel, andMenuSeparatorfor grouped sectionsMenuItemfor actionable rows with optional leading and trailing adornmentsMenuSub,MenuSubTrigger, andMenuSubContentfor nested secondary menus
Menus integrate with @edux-design/buttons for triggers and @edux-design/popovers for positioning.
Shared overlay and row-emphasis motion comes from @edux-design/tokens.
Installation
pnpm add @edux-design/menus @edux-design/buttons @edux-design/popovers @edux-design/utils
# or
npm install @edux-design/menus @edux-design/buttons @edux-design/popovers @edux-design/utilsPeer deps also include react@^19.1.0 and react-dom@^19.1.0.
Usage
import {
Menu,
MenuList,
MenuGroup,
MenuItem,
MenuSub,
MenuSubTrigger,
MenuSubContent,
} from "@edux-design/menus";
export function InsertMenu() {
return (
<Menu
button="Insert"
contentProps={{ className: "min-w-[220px]" }}
trapFocus={false}
disableOutsidePointerEvents={false}
onOpenAutoFocus={(event) => event.preventDefault()}
>
<MenuList>
<MenuItem startAdornment={<span>Aa</span>}>Heading</MenuItem>
<MenuItem endAdornment={<span>Ctrl+K</span>}>
Command palette
</MenuItem>
<MenuSub>
<MenuSubTrigger startAdornment={<span>#</span>}>
Layout
</MenuSubTrigger>
<MenuSubContent>
<MenuList className="min-w-[200px]">
<MenuItem>Two columns</MenuItem>
<MenuItem>Three columns</MenuItem>
</MenuList>
</MenuSubContent>
</MenuSub>
</MenuList>
</Menu>
);
}Grouped sections can either be composed explicitly with MenuGroup, or inferred automatically from group, groupLabel, and groupOrder on direct MenuItem and MenuSub children.
For context-menu use cases, control the menu externally and pass anchorPoint={{ x, y }} from the pointer event instead of rendering a trigger button.
import { useState } from "react";
import { Menu, MenuList, MenuItem } from "@edux-design/menus";
export function CanvasContextMenu() {
const [anchorPoint, setAnchorPoint] = useState(null);
return (
<>
<div
onContextMenu={(event) => {
event.preventDefault();
setAnchorPoint({ x: event.clientX, y: event.clientY });
}}
>
Right-click me
</div>
<Menu
open={Boolean(anchorPoint)}
onOpenChange={(nextOpen) => {
if (!nextOpen) {
setAnchorPoint(null);
}
}}
anchorPoint={anchorPoint}
>
<MenuList>
<MenuItem onClick={() => setAnchorPoint(null)}>Cut</MenuItem>
<MenuItem onClick={() => setAnchorPoint(null)}>Copy</MenuItem>
</MenuList>
</Menu>
</>
);
}Component API
Menu
- Accepts either a string trigger, which renders an
@edux-design/buttonsButton, or a custom React node. buttonis optional when usinganchorPointfor a triggerless context menu.- Supports controlled and uncontrolled modes via
open,defaultOpen, andonOpenChange. - Use
anchorPoint={{ x, y }}to position the root menu directly from viewport coordinates such as a right-click event. - Forwards
align,side,trapFocus,disableOutsidePointerEvents, and other content props to the underlying popover. - Applies the shared
.eds-motion-overlayrecipe from@edux-design/tokensby default.
MenuItem
- Use
startAdornmentfor a leading icon, badge, or label. - Use
endAdornmentfor a shortcut hint, status badge, or custom trailing content. - Use
disabledto make an item non-interactive. - Use
closeOnSelect={false}when selecting the item should not dismiss the full menu tree. - Use
group,groupLabel, andgroupOrderwhen the list should auto-build grouped sections. - Uses the shared
.eds-motion-row-emphasisrecipe from@edux-design/tokensfor hover and focus emphasis.
MenuGroup
- Wrap related items in
MenuGroupto render a section label and divider without repeating group props on each child. - Use
labelfor the heading text andorderto sort groups when multiple groups are present in one list. - Use
MenuGroupLabelandMenuSeparatordirectly when you need fully manual structure.
MenuSub
MenuSubmanages a nested secondary menu.MenuSubTriggerrenders like a normalMenuItemand adds a trailing submenu indicator by default.MenuSubContentpositions the nested panel to the right of the trigger by default.MenuSubalso supportsgroup,groupLabel, andgroupOrderwhen it is a direct child ofMenuList.
Keyboard behavior
ArrowDownandArrowUploop through items in the active menu list.HomeandEndjump to the first and last enabled items.- Typeahead moves focus to the next matching item label.
EnterandSpaceactivate the focused item.ArrowRightopens a focused submenu trigger.ArrowLeftandEscapeclose the active submenu and return focus to its trigger.Tabcloses the menu so focus can leave naturally.- Selecting a standard
MenuItemcloses the full open menu tree.
Development
pnpm --filter @edux-design/menus lint
pnpm --filter @edux-design/menus check-types
pnpm --filter @edux-design/menus buildStorybook examples live in src/demos/Menu.stories.jsx.
Notes
- Menu items render as
<li role="menuitem">rows inside arole="menu"list. - When integrating with routing libraries, keep navigation inside the item
onClick. - Menu animations are applied via the
data-[state=open|closed]classes on the popover content.
