UNPKG

@dnb/eufemia

Version:

DNB Eufemia Design System UI Library

478 lines (427 loc) 12.6 kB
--- title: 'Menu' description: 'Menu is a composable dropdown menu component for actions and navigation, with keyboard navigation, nested menus, and full accessibility support.' version: 11.0.0 generatedAt: 2026-04-21T13:57:51.571Z checksum: 1fc99517592ef537de08c221f7492a20a5f0da34d4c6422f3025968fc4f94199 --- # Menu ## Import ```tsx import { Menu } from '@dnb/eufemia' ``` ## Description Menu provides an accessible dropdown menu for actions and navigation with a composable, tree-shakeable API. Use `Menu.Root` as the wrapper, `Menu.Button` for the trigger, `Menu.List` for the list container, `Menu.Action` for individual items, and `Menu.Divider` for visual separators. `Menu.Button` supports all [Button](/uilib/components/button/properties) props (e.g. `text`, `icon`, `variant`, `size`, `disabled`), so you can customise the trigger the same way you would with a regular Button. Nested menus are supported by nesting another `Menu.Root` inside `Menu.List`use a `Menu.Action` as the direct child of the nested `Menu.Root` to serve as the sub-menu trigger. For inline expandable groups, use `Menu.Accordion` instead of a nested `Menu.Root`. It reveals child items with a height animation inside the current menu, rather than opening a separate popover. ## Relevant links - Source code: https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-eufemia/src/components/menu - Docs code: https://github.com/dnbexperience/eufemia/tree/main/packages/dnb-design-system-portal/src/docs/uilib/components/menu ## Accessibility - The menu uses ARIA `role="menu"` and `role="menuitem"` semantics. - The trigger receives `aria-haspopup="menu"` and `aria-expanded` attributes automatically. - Keyboard navigation follows the [WAI-ARIA Menu Pattern](https://www.w3.org/WAI/ARIA/apd/patterns/menu/): - **Arrow Up/Down**: Move focus between items (wraps around). - **Home/End**: Jump to first/last item. - **Enter/Space**: Activate the focused item. - **Escape**: Close the menu. - **Tab**: Close the menu and move focus naturally. - **Arrow Right**: Open a sub-menu (when the item has one). - **Arrow Left**: Close a sub-menu and return to the parent. - Type-ahead: pressing a letter key jumps to the first matching item. - Focus is moved to the menu container when it opens. Arrow keys then move focus to individual items. Focus returns to the trigger when the menu closes. - Disabled items receive `aria-disabled` and are skipped during keyboard navigation. - Dividers use `role="separator"`. ## Demos ### Basic Menu ```tsx render( <Menu.Root> <Menu.Button /> <Menu.List> <Menu.Action text="Action" onClick={() => null} /> <Menu.Action text="Link" href="https://www.dnb.no/" /> </Menu.List> </Menu.Root> ) ``` ### Accordion ```tsx render( <Menu.Root> <Menu.Button text="File" icon="chevron_down" /> <Menu.List> <Menu.Action icon={file_add} text="New" onClick={() => console.log('new')} /> <Menu.Action icon={folder} text="Open" onClick={() => console.log('open')} /> <Menu.Divider /> <Menu.Accordion icon={folder} text="Export as"> <Menu.Action icon={file_pdf} text="PDF" onClick={() => console.log('export pdf')} /> <Menu.Action icon={file_png} text="PNG" onClick={() => console.log('export png')} /> </Menu.Accordion> <Menu.Divider /> <Menu.Action icon={save} text="Save" onClick={() => console.log('save')} /> </Menu.List> </Menu.Root> ) ``` ### Nested Menu ```tsx render( <Menu.Root arrowPosition="left"> <Menu.Button text="File" icon="chevron_down" /> <Menu.List> <Menu.Action icon={file_add} text="New" onClick={() => console.log('new')} /> <Menu.Action icon={folder} text="Open" onClick={() => console.log('open')} /> <Menu.Divider /> <Menu.Root placement="right" arrowPosition="top"> <Menu.Action icon={folder} text="Export as" /> <Menu.List> <Menu.Action icon={file_pdf} text="PDF" onClick={() => console.log('export pdf')} /> <Menu.Action icon={file_png} text="PNG" onClick={() => console.log('export png')} /> <Menu.Action icon={file} text="SVG" onClick={() => console.log('export svg')} /> </Menu.List> </Menu.Root> <Menu.Divider /> <Menu.Action icon="close" text="Close" onClick={() => console.log('close')} /> </Menu.List> </Menu.Root> ) ``` ### With Links ```tsx render( <Menu.Root> <Menu.Button text="Navigate" icon="chevron_down" variant="tertiary" /> <Menu.List> <Menu.Action icon={home} text="Home" href="/" /> <Menu.Action icon={layout_card} text="Dashboard" href="/dashboard" /> <Menu.Action icon={launch} text="External" href="https://example.com" target="_blank" rel="noopener noreferrer" /> </Menu.List> </Menu.Root> ) ``` ### Max Visible List Items ```tsx render( <Menu.Root> <Menu.Button text="Long list" icon="chevron_down" /> <Menu.List maxVisibleListItems={4}> <Menu.Action text="Item 1" /> <Menu.Action text="Item 2" /> <Menu.Action text="Item 3" /> <Menu.Action text="Item 4" /> <Menu.Action text="Item 5" /> <Menu.Action text="Item 6" /> <Menu.Action text="Item 7" /> <Menu.Action text="Item 8" /> </Menu.List> </Menu.Root> ) ``` ### With Headers ```tsx render( <Menu.Root> <Menu.Button text="Edit" icon="chevron_down" /> <Menu.List> <Menu.Header text="Clipboard" /> <Menu.Action icon={scissors} text="Cut" onClick={() => console.log('cut')} /> <Menu.Action icon={copy} text="Copy" onClick={() => console.log('copy')} /> <Menu.Action icon={edit} text="Paste" disabled /> <Menu.Divider /> <Menu.Header text="Selection" /> <Menu.Action icon="check" text="Select All" /> </Menu.List> </Menu.Root> ) ``` ## Menu.Root ```json { "props": { "open": { "doc": "Controlled open state. Use together with `onOpenChange`.", "type": "boolean", "status": "optional" }, "arrowPosition": { "doc": "Position of the popover arrow relative to the popover. `top` and `bottom` positions are only applicable when `placement` is `left` or `right`, and vice versa.", "type": [ "\"left\"", "\"right\"", "\"center\"", "\"top\"", "\"bottom\"" ], "defaultValue": "\"center\"", "status": "optional" }, "placement": { "doc": "Preferred placement of the menu relative to the trigger.", "type": ["\"top\"", "\"right\"", "\"bottom\"", "\"left\""], "defaultValue": "\"bottom\"", "status": "optional" }, "autoAlignMode": { "doc": "Control when the menu automatically flips its placement to fit within the viewport. `\"initial\"`: flip only on open. `\"scroll\"`: also flip during scroll. `\"never\"`: always use specified placement.", "type": ["\"initial\"", "\"scroll\"", "\"never\""], "defaultValue": "\"initial\"", "status": "optional" }, "skipPortal": { "doc": "Render inline instead of inside a portal.", "type": "boolean", "defaultValue": "false", "status": "optional" }, "noAnimation": { "doc": "Disable the open/close animation.", "type": "boolean", "defaultValue": "false", "status": "optional" } } } ``` ## Menu.Button ```json { "props": { "icon": { "doc": "Icon displayed on the trigger button.", "type": "IconIcon", "defaultValue": "\"more\"", "status": "optional" }, "variant": { "doc": "Button variant.", "type": ["\"primary\"", "\"secondary\"", "\"tertiary\""], "defaultValue": "\"secondary\"", "status": "optional" }, "text": { "doc": "Visible text label for the trigger button.", "type": "string", "status": "optional" }, "[Button props]": { "doc": "All [Button](/uilib/components/button/properties) props are supported.", "type": "Various", "status": "optional" } } } ``` ## Menu.List ```json { "props": { "children": { "doc": "Menu items. Use `Menu.Action` and `Menu.Divider` as direct children.", "type": "React.ReactNode", "status": "required" }, "maxVisibleListItems": { "doc": "Sets the maximum visible list items before the content scrolls. The component measures the rendered height of the first visible items. An explicit `style.maxHeight` overrides this.", "type": "number", "status": "optional" } } } ``` ## Menu.Action ```json { "props": { "text": { "doc": "Action label text.", "type": "React.ReactNode", "status": "optional" }, "icon": { "doc": "Icon displayed before the text.", "type": "IconIcon", "status": "optional" }, "href": { "doc": "When provided, the action renders as a link.", "type": "string", "status": "optional" }, "to": { "doc": "Use this property when using a router Link component as the `element`. The `to` value is passed to the router element for client-side navigation.", "type": "string", "status": "optional" }, "element": { "doc": "Define what HTML or React element should be used for the link (e.g. `element={Link}` for a router Link component). Defaults to a semantic `a` element.", "type": "React.Element", "status": "optional" }, "target": { "doc": "Link target attribute (e.g. `_blank`).", "type": "string", "status": "optional" }, "rel": { "doc": "Link rel attribute (e.g. `noopener noreferrer`).", "type": "string", "status": "optional" }, "disabled": { "doc": "Disables the action. Sets `aria-disabled` and prevents click/keyboard activation.", "type": "boolean", "defaultValue": "false", "status": "optional" }, "children": { "doc": "Custom content rendered inside the action item.", "type": "React.ReactNode", "status": "optional" } } } ``` ## Menu.Accordion ```json { "props": { "text": { "doc": "Accordion trigger label text.", "type": "React.ReactNode", "status": "optional" }, "icon": { "doc": "Icon displayed before the text.", "type": "IconIcon", "status": "optional" }, "disabled": { "doc": "Disables the accordion trigger. Sets `aria-disabled` and prevents toggling.", "type": "boolean", "defaultValue": "false", "status": "optional" }, "children": { "doc": "Menu items rendered inside the accordion when open. Use `Menu.Action` and `Menu.Divider` as children.", "type": "React.ReactNode", "status": "required" } } } ``` ## Menu.Header ```json { "props": { "text": { "doc": "Header text displayed in the menu.", "type": "React.ReactNode", "status": "optional" }, "children": { "doc": "Alternative to `text`. Content rendered inside the header.", "type": "React.ReactNode", "status": "optional" } } } ``` ## Menu.Divider No properties. ## Menu.Root Events ```json { "props": { "onOpenChange": { "doc": "Called whenever the open state changes. Receives the new open state as a boolean.", "type": "(open: boolean) => void", "status": "optional" } } } ``` ## Menu.Action Events ```json { "props": { "onClick": { "doc": "Called when the action is clicked or activated via keyboard (Enter/Space). The menu closes automatically after the handler is invoked unless used as a trigger for a nested `Menu.Root`.", "type": "(event: React.MouseEvent<HTMLLIElement>) => void", "status": "optional" } } } ``` ## Menu.Accordion Events ```json { "props": { "onOpenChange": { "doc": "Called whenever the accordion open state changes. Receives the new open state as a boolean.", "type": "(open: boolean) => void", "status": "optional" } } } ```