Menu

Displays a menu to the user, such as a set of actions or functions, triggered by a button.

Anatomy

Import and assemble the component:

1import { Menu } from '@raystack/apsara'
2
3<Menu>
4 <Menu.Trigger />
5 <Menu.Content>
6 <Menu.Item />
7 </Menu.Content>
8</Menu>

Usage

Beyond the plain case, items can carry icons, sit in groups, nest into submenus, and narrow down as someone types.

Basic usage

A trigger and a list of items. The menu opens on click and closes on select, Escape, or an outside click.

1<Menu>
2 <Menu.Trigger render={<Button color="neutral" />}>Open Menu</Menu.Trigger>
3 <Menu.Content>
4 <Menu.Item>Profile</Menu.Item>
5 <Menu.Item>Settings</Menu.Item>
6 <Menu.Separator />
7 <Menu.Item>Logout</Menu.Item>
8 </Menu.Content>
9</Menu>

With icons

You can add icons to the menu items. Supports both leading and trailing icons.

1<Menu>
2 <Menu.Trigger render={<Button color="neutral" />}>Actions</Menu.Trigger>
3 <Menu.Content>
4 <Menu.Item leadingIcon={<>📝</>}>Edit</Menu.Item>
5 <Menu.Item leadingIcon={<>📋</>} trailingIcon={<>⌘C</>}>
6 Copy
7 </Menu.Item>
8 <Menu.Separator />
9 <Menu.Item leadingIcon={<>🗑️</>}>Delete</Menu.Item>
10 </Menu.Content>
11</Menu>

With groups and labels

Organize related menu items into sections with descriptive headers.

1<Menu>
2 <Menu.Trigger render={<Button color="neutral" />}>More</Menu.Trigger>
3 <Menu.Content>
4 <Menu.Group>
5 <Menu.Label>Actions</Menu.Label>
6 <Menu.Item>New File</Menu.Item>
7 <Menu.Item>New Folder</Menu.Item>
8 </Menu.Group>
9 <Menu.Separator />
10 <Menu.Group>
11 <Menu.Label>Sort By</Menu.Label>
12 <Menu.Item>Name</Menu.Item>
13 <Menu.Item>Date</Menu.Item>
14 </Menu.Group>

Use Menu.Submenu, Menu.SubmenuTrigger, and Menu.SubmenuContent to create nested menus with multiple levels.

1<Menu>
2 <Menu.Trigger render={<Button color="neutral" />}>Basic Menu</Menu.Trigger>
3 <Menu.Content>
4 <Menu.Item>Profile</Menu.Item>
5 <Menu.Item>Settings</Menu.Item>
6 <Menu.Separator />
7 <Menu.Item>Logout</Menu.Item>
8 <Menu.Submenu>
9 <Menu.SubmenuTrigger>Sub Menu</Menu.SubmenuTrigger>
10 <Menu.SubmenuContent>
11 <Menu.Item>Sub Menu Item 1</Menu.Item>
12 <Menu.Item>Sub Menu Item 2</Menu.Item>
13 <Menu.Item>Sub Menu Item 3</Menu.Item>
14 </Menu.SubmenuContent>

Checkbox items

Use Menu.CheckboxItem for a setting that turns on or off. Clicking it toggles the check mark and keeps the menu open. Pass checked and onCheckedChange to control it.

1(function CheckboxMenu() {
2 const [showGrid, setShowGrid] = React.useState(true);
3
4 return (
5 <Menu>
6 <Menu.Trigger render={<Button color="neutral" />}>View</Menu.Trigger>
7 <Menu.Content>
8 <Menu.CheckboxItem checked={showGrid} onCheckedChange={setShowGrid}>
9 Show grid
10 </Menu.CheckboxItem>
11 <Menu.CheckboxItem defaultChecked>Show rulers</Menu.CheckboxItem>
12 <Menu.CheckboxItem disabled>Show guides</Menu.CheckboxItem>
13 </Menu.Content>
14 </Menu>

Radio items

Wrap Menu.RadioItem parts in a Menu.RadioGroup. Only one item in the group is checked at a time. Clicking an item keeps the menu open.

1(function RadioMenu() {
2 const [layout, setLayout] = React.useState("list");
3
4 return (
5 <Menu>
6 <Menu.Trigger render={<Button color="neutral" />}>Layout</Menu.Trigger>
7 <Menu.Content>
8 <Menu.RadioGroup value={layout} onValueChange={setLayout}>
9 <Menu.Label>Layout</Menu.Label>
10 <Menu.RadioItem value="list">List</Menu.RadioItem>
11 <Menu.RadioItem value="board">Board</Menu.RadioItem>
12 <Menu.RadioItem value="timeline">Timeline</Menu.RadioItem>
13 </Menu.RadioGroup>
14 </Menu.Content>

Use Menu.LinkItem to navigate. It renders an <a> element. The menu stays open on click by default. Pass closeOnClick to close it, for example with client-side routing.

To use a router link, pass it to render. The item keeps its layout and icons.

1<Menu.LinkItem render={<NextLink href="/projects" />}>Projects</Menu.LinkItem>
1<Menu>
2 <Menu.Trigger render={<Button color="neutral" />}>Go to</Menu.Trigger>
3 <Menu.Content>
4 <Menu.LinkItem href="#projects">Projects</Menu.LinkItem>
5 <Menu.LinkItem href="#settings">Settings</Menu.LinkItem>
6 <Menu.LinkItem
7 href="https://github.com/raystack/apsara"
8 target="_blank"
9 rel="noreferrer"
10 closeOnClick
11 >
12 GitHub
13 </Menu.LinkItem>
14 </Menu.Content>

Autocomplete

Pass autocomplete to the Menu root. Each menu manages its own autocomplete state.

  • autocompleteMode="auto" is the default. Items filter as the user types. Each item matches by its value prop, or by its children text when it has no value. Matching ignores case and accents.
  • autocompleteMode="manual" leaves filtering to you. Do it in onInputValueChange.
  • Submenus opt in separately: pass autocomplete to Menu.Submenu.
1<Menu autocomplete>
2 <Menu.Trigger render={<Button color="neutral" />}>
3 Default Autocomplete
4 </Menu.Trigger>
5 <Menu.Content searchPlaceholder="Search">
6 <Menu.EmptyState>No results found</Menu.EmptyState>
7 <Menu.Group>
8 <Menu.Label>Heading</Menu.Label>
9 <Menu.Item>Assign member...</Menu.Item>
10 <Menu.Item>Subscribe...</Menu.Item>
11 <Menu.Item>Rename...</Menu.Item>
12 </Menu.Group>
13 <Menu.Separator />
14 <Menu.Group>

Linear inspired Menu

This is a Linear-inspired menu component that supports custom filtering and displays nested options. Users can search through all nested items using a single input field.

To closely replicate Linear-style filtering, the filtering logic should include result ranking. Using a utility like match-sorter can help achieve this by sorting filtered results based on relevance.

1(function LinearMenuDemo() {
2 const [searchQuery, setSearchQuery] = useState("");
3
4 const renderMenu = (items: MenuItem[], query: string) => {
5 const filteredItems = filterMenuItems(items, query);
6
7 if (searchQuery && filteredItems.length === 0) {
8 return <div className="no-results">No results</div>;
9 }
10
11 return filteredItems.map((item, index) => {
12 switch (item.type) {
13 case "group":
14 return (

Controlled

Pass open with onOpenChange to own the state. Do this when a keyboard shortcut or another control has to open the menu.

1(function ControlledMenu() {
2 const [open, setOpen] = React.useState(false);
3
4 return (
5 <Flex align="center" gap={5}>
6 <Menu open={open} onOpenChange={setOpen}>
7 <Menu.Trigger render={<Button variant="outline" />}>
8 Actions
9 </Menu.Trigger>
10 <Menu.Content>
11 <Menu.Item onClick={() => setOpen(false)}>Rename</Menu.Item>
12 <Menu.Item onClick={() => setOpen(false)}>Duplicate</Menu.Item>
13 </Menu.Content>
14 </Menu>

API Reference

The Menu component is composed of several parts, each with their own props.

Root

The root element is the parent component that holds the menu. Using the autocomplete prop, you can enable search functionality.

Prop

Type

Trigger

The button that triggers the menu.

By default, the click event is not propagated. You can override this behavior by passing stopPropagation={false}.

Use the render prop to render a custom trigger element.

Prop

Type

Content

The container that holds the menu items. When autocomplete is enabled, renders an inline search input inside the popup with a search input.

Prop

Type

Item

Individual clickable options within the menu. Built on top of Base UI Menu.Item.

In a searchable menu, the item's children text is used for matching. Pass a value prop to match a different text.

Prop

Type

CheckboxItem

A menu item that toggles a setting on or off. Renders a check mark before the label when checked. Built on Base UI Menu.CheckboxItem.

Prop

Type

RadioGroup

Groups related radio items and holds the selected value.

Prop

Type

RadioItem

A menu item that works like a radio button in a Menu.RadioGroup. Renders a dot before the label when checked.

Prop

Type

LinkItem

A menu item that renders an <a> element. Built on Base UI Menu.LinkItem.

Prop

Type

Group

A way to group related menu items together. When filtering is active, a group with no matching items is hidden.

Prop

Type

Label

Renders a label in a menu group. This component should be wrapped with Menu.Group so the aria-labelledby is correctly set on the group element.

Prop

Type

Separator

Visual divider between menu items or groups. Hidden when filtering is active.

Prop

Type

EmptyState

Placeholder content when there are no menu items to display. In a searchable menu, it shows only when no item matches the search, and screen readers announce it.

Prop

Type

Wraps a submenu root. Use with Menu.SubmenuTrigger and Menu.SubmenuContent to create nested menus.

Supports its own autocomplete prop to enable search functionality within the submenu independently from the parent menu.

Prop

Type

The trigger item for a submenu. Renders with a trailing chevron icon by default. Accepts leadingIcon and trailingIcon props.

When inside a searchable parent menu, the trigger matches by its value prop, or by its children text when it has no value. The root search does not search inside submenus.

Prop

Type

The content container for a submenu. Shares the same API as Menu.Content with a default sideOffset of 2.

Prop

Type

Slots

Every rendered part carries a stable data-slot attribute for styling and testing:

SlotElement
menu-triggerThe Menu.Trigger element
menu-positionerPositioning wrapper around the content popup
menu-contentThe Menu.Content / Menu.SubmenuContent popup
menu-search-inputThe autocomplete search input (when autocomplete)
menu-search-listWrapper around items in autocomplete mode (when autocomplete)
menu-itemEach Menu.Item
menu-cell-leading-iconLeading icon inside an item or submenu trigger (when provided)
menu-cell-trailing-iconTrailing icon inside an item or submenu trigger (when provided)
menu-groupMenu.Group wrapper
menu-labelMenu.Label
menu-separatorMenu.Separator
menu-checkbox-itemEach Menu.CheckboxItem
menu-checkbox-item-indicatorCheck mark inside a checkbox item
menu-radio-groupMenu.RadioGroup wrapper
menu-radio-itemEach Menu.RadioItem
menu-radio-item-indicatorDot inside a radio item
menu-link-itemEach Menu.LinkItem
menu-empty-stateMenu.EmptyState
menu-subtriggerMenu.SubmenuTrigger

Accessibility

  • Built on Base UI Menu, following the WAI-ARIA Menu pattern: the trigger exposes aria-haspopup and aria-expanded, the popup uses role="menu", and items use role="menuitem".
  • Arrow keys move through items, Enter activates the highlighted item, Escape closes the menu and returns focus to the trigger, and ArrowRight/ArrowLeft open and close submenus.
  • With autocomplete, the trigger has aria-haspopup="dialog" and the popup becomes a role="dialog" containing a search input (role="searchbox", labelled by searchPlaceholder) and a role="menu" list. Items keep their menu roles. Focus stays in the input while arrow keys move the highlight. ArrowRight opens a highlighted submenu and moves focus to its first item, or to its own search input when it is searchable. Escape closes the submenu and returns focus to the parent input.
  • Menu.Label must be wrapped in Menu.Group so the group is labelled via aria-labelledby.
  • Menu.CheckboxItem uses role="menuitemcheckbox" and Menu.RadioItem uses role="menuitemradio", both with aria-checked.
  • Menu.LinkItem renders an <a> with role="menuitem".
  • In a searchable menu, Menu.EmptyState is a role="status" live region.