Menu
- Stable
- WCAG 2.2 evidence
- RTL
- Web · iOS · Android
A list of actions or options that opens from a trigger.
Built on React Aria MenuTrigger + Popover + Menu: the trigger gets aria-haspopup and aria-expanded; Enter,
Space and the arrow keys open the menu with focus on the first (or last) item; arrows, Home/End and type-ahead move
focus; Escape closes it and returns focus to the trigger; submenus open with the arrow key that points into them
(mirrored in RTL) (WCAG 2.1.1, 2.4.3, 4.1.2). Long menus scroll inside the surface, which flips to stay in the viewport. Two ways to use it: pass trigger (simple), or compose MenuTrigger > trigger + Menu, and SubmenuTrigger >
MenuItem + Menu for submenus. Put primary, frequent actions on buttons instead.
import { Menu } from "@nexera-ui/react";- 5
- examples
- 28
- props
- 3
- live controls
- 3
- platforms
- 7
- WCAG criteria
- 2
- blocks use it
Try every prop. Copy the code.
Change the props and the code updates. Check light and dark, LTR and RTL, and phone width.
import { Menu } from "@nexera-ui/react";
<Menu />Examples 4
The same examples as Storybook, rendered live. Open Code to copy one.
Sections and selection
Composable form: MenuTrigger > trigger + Menu, sections with MenuSectionLabel, and single selection.
import { useState } from "react";
import {
Button,
Menu,
MenuItem,
MenuSection,
MenuSectionLabel,
MenuTrigger,
} from "@nexera-ui/react";
import { LuChevronDown } from "react-icons/lu";
export function SectionsAndSelection() {
function Example() {
const [sort, setSort] = useState<"name" | "date" | "size">("name");
return (
<MenuTrigger>
<Button variant="secondary" trailingIcon={<LuChevronDown />}>
Sort
</Button>
<Menu
selectionMode="single"
selectedKeys={[sort]}
onSelectionChange={(keys) => {
const [key] = keys === "all" ? [] : [...keys];
if (key === "name" || key === "date" || key === "size") setSort(key);
}}
>
<MenuSection>
<MenuSectionLabel label="Sort by" />
<MenuItem id="name" label="Name" />
<MenuItem id="date" label="Date modified" />
<MenuItem id="size" label="Size" />
</MenuSection>
</Menu>
</MenuTrigger>
);
}
return <Example />;
}
Controlled
Controlled open state: isOpen and onOpenChange.
import { useState } from "react";
import { Button, Menu, MenuDivider, MenuItem, SubmenuTrigger } from "@nexera-ui/react";
import { LuCopy, LuDownload, LuLink, LuMail, LuPencil, LuTrash2, LuUsers } from "react-icons/lu";
function FigmaItems() {
return (
<>
<MenuItem id="edit" label="Edit" icon={<LuPencil />} shortcut="E" />
<MenuItem id="duplicate" label="Duplicate" icon={<LuCopy />} shortcut="D" />
<SubmenuTrigger>
<MenuItem id="share" label="Share with" icon={<LuUsers />} />
<Menu>
<MenuItem id="email" label="Email" icon={<LuMail />} />
<MenuItem id="link" label="Copy link" icon={<LuLink />} />
</Menu>
</SubmenuTrigger>
<MenuItem id="download" label="Download" icon={<LuDownload />} />
<MenuDivider />
<MenuItem id="delete" label="Delete" type="destructive" icon={<LuTrash2 />} />
</>
);
}
export function Controlled() {
function Example() {
const [open, setOpen] = useState(false);
return (
<div className="flex items-center gap-4">
<Menu
trigger={<Button variant="secondary">Actions</Button>}
isOpen={open}
onOpenChange={setOpen}
>
<FigmaItems />
</Menu>
<span className="text-body-small text-secondary">{open ? "Open" : "Closed"}</span>
</div>
);
}
return <Example />;
}
Long menu
Long menus scroll inside the surface; the focused item is scrolled into view with room for its focus ring.
import { Button, Menu, MenuItem } from "@nexera-ui/react";
export function LongMenu() {
return (
<Menu trigger={<Button variant="secondary">Country</Button>}>
{Array.from({ length: 40 }, (_, index) => (
<MenuItem key={index} id={`c${String(index)}`} label={`Country ${String(index + 1)}`} />
))}
</Menu>
);
}
Platforms
Figma System menu: the same menu on iOS (250 pt, radius 14, group separators) and Android (220 dp, radius 4).
import { Button, Menu, MenuDivider, MenuItem } from "@nexera-ui/react";
import { LuCopy, LuShare2, LuTrash2 } from "react-icons/lu";
const platforms = ["web", "ios", "android"] as const;
export function Platforms() {
return (
<div className="flex flex-wrap items-center gap-4">
{platforms.map((platform) => (
<Menu
key={platform}
platform={platform}
trigger={<Button variant="secondary">{`Open ${platform} menu`}</Button>}
>
<MenuItem id="share" label="Share" icon={<LuShare2 />} />
<MenuItem id="copy" label="Copy" icon={<LuCopy />} />
<MenuDivider />
<MenuItem id="delete" label="Delete" type="destructive" icon={<LuTrash2 />} />
</Menu>
))}
</div>
);
}
Props 28
Press "Try it" on a card to load that prop into the playground.
28 props shown
platformNexera"web" | "ios" | "android"Platform look. Items, section labels
and dividers inside follow it. Defaults to the enclosing menu (for submenus), then the NexeraProvider.
placementNexera"bottom" | "bottom left" | "bottom right" | "bottom start" | "bottom end" | "top" | "top left" | "top right" | "top start" | "top end" | "left" | "left top" | "left bottom" | "start" | "start top" | "start bottom" | "right" | "right top" | "right bottom" | "end" | "end top" | "end bottom"Where the menu opens relative to its trigger. It flips to stay inside the viewport. Submenus default to
"end top", which mirrors in right-to-left layouts.
classNameNexerastringExtra classes for the menu surface, merged last so they win over the defaults.
styleNexeraCSSPropertiesInline style for the menu surface.
triggerNexeraReactElement<unknown, string | JSXElementConstructor<any>>The element that opens the menu, usually a Button or IconButton. React Aria gives it aria-haspopup,
aria-expanded, and opens the menu on press, Enter, Space and the arrow keys. Without trigger, render the
Menu as the second child of a MenuTrigger (or of a SubmenuTrigger for a submenu).
isOpenNexerabooleanWhether the menu is open (controlled). Use with onOpenChange.
defaultOpenNexerabooleanWhether the menu is open on first render (uncontrolled).
onOpenChangeNexera(isOpen: boolean) => voidCalled when the menu opens or closes.
childrenReact AriaReactNode | ((item: T) => ReactNode)The contents of the collection.
escapeKeyBehaviorReact Aria"clearSelection" | "none"Whether pressing the escape key should clear selection in the menu or not. Most experiences should not modify this option as it eliminates a keyboard user's ability to easily clear selection. Only use if the escape key is being handled externally or should not trigger selection clearing contextually.
autoFocusReact Ariaboolean | FocusStrategyWhere the focus should be set.
shouldFocusWrapReact AriabooleanWhether keyboard navigation is circular.
onActionReact Aria(key: Key, value: T) => voidHandler that is called when an item is selected.
onCloseReact Aria() => voidHandler that is called when the menu should close after selecting an item.
itemsReact AriaIterable<T>Item objects in the collection.
disabledKeysReact AriaIterable<Key>The item keys that are disabled. These items cannot be selected, focused, or otherwise interacted with.
selectionModeReact Aria"none" | "single" | "multiple"The type of selection that is allowed in the collection.
disallowEmptySelectionReact AriabooleanWhether the collection allows empty selection.
selectedKeysReact AriaIterable<Key> | "all"The currently selected keys in the collection (controlled).
defaultSelectedKeysReact AriaIterable<Key> | "all"The initial selected keys in the collection (uncontrolled).
onSelectionChangeReact Aria(keys: Selection) => voidHandler that is called when the selection changes.
idReact AriastringThe element's unique identifier. See [MDN](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/id).
aria-labelReact AriastringDefines a string value that labels the current element.
aria-labelledbyReact AriastringIdentifies the element (or elements) that labels the current element.
aria-describedbyReact AriastringIdentifies the element (or elements) that describes the object.
renderEmptyStateReact Aria() => ReactNodeProvides content to display when there are no items in the menu.
shouldCloseOnSelectReact AriabooleanWhether the menu should close when the menu item is selected.
dependenciesReact Ariareadonly any[]Values that should invalidate the item cache when using dynamic collections.
* Required. React Aria props shown are the ones most apps use; the component accepts the rest of its React Aria props too.
Accessible by default.
Built on React Aria, and covered by the WCAG 2.2 evidence generated on every build.
WCAG 2.2 evidence
5 direct · 2 supporting- 1.3.1Info and RelationshipsLevel A · tested directly
- 1.3.2Meaningful SequenceLevel A · tested directly
- 1.4.10ReflowLevel AA · supporting test
- 2.1.1KeyboardLevel A · tested directly
- 2.3.3Animation from InteractionsLevel AAA · supporting test
- 2.4.3Focus OrderLevel A · tested directly
- 4.1.2Name, Role, ValueLevel A · tested directly
Fits any width.
Nexera components respond to the space they are given. Drag the corner of the frame, or pick a width.
Styling hooks
Pass className to add Tailwind classes (merged last). State is exposed as data attributes, so you can style it with variants like data-pressed:.
<Menu className="shadow-sm" />Used in blocks
Related components
- BoardCardA Kanban card: avatar, name, role, drag handle and a detail line.
- ButtonTriggers an action such as Save, Submit, Approve or Delete.
- ButtonGroupItemOne toggle in a joined row of buttons, for example a List / Grid view switch or a set of filters.
- ContextMenuThe menu of actions for an element, opened where the user asks for it: right-click, long-press on touch screens, and from the keyboard with Shift+F10 or the ContextMenu key (the browser reports both as a `contextmenu` event on the focused element; on macOS, Control+Enter).
- DragHandleGrip that starts a drag of the collection item it sits in.
- DropIndicatorInsertion line shown between two items while something is dragged over a `GridList` or `ListBox`.
- IconButtonA square, icon-only button for compact actions such as Settings, Close or Delete row.
- MenuDividerSeparates groups of items in a `Menu` or `ContextMenu`.