Skip to main content

Overview

Menu provides a dropdown menu with extensive features including nested submenus, keyboard navigation, grouping, separators, and optional autocomplete filtering for large menus.

Basic usage

import { Menu } from '@raystack/apsara';

<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Item onSelect={() => console.log('Item 1')}>Item 1</Menu.Item>
    <Menu.Item onSelect={() => console.log('Item 2')}>Item 2</Menu.Item>
    <Menu.Separator />
    <Menu.Item onSelect={() => console.log('Item 3')}>Item 3</Menu.Item>
  </Menu.Content>
</Menu>

Components

The root component that manages menu state and keyboard navigation.
open
boolean
Controls the open state of the menu (controlled mode).
defaultOpen
boolean
The initial open state in uncontrolled mode.
onOpenChange
(open: boolean) => void
Callback fired when the open state changes.
modal
boolean
default:"true"
Whether the menu is modal. Always true, cannot be overridden.
loopFocus
boolean
default:"false"
Whether to loop focus when navigating with arrow keys. Always false, cannot be overridden.
autocomplete
boolean
Enable autocomplete mode with a search input at the top of the menu.
autocompleteMode
'auto' | 'manual'
default:"'auto'"
Auto mode: Automatically filters menu items based on the input value. Manual mode: Provides input value but doesn’t filter items automatically.
inputValue
string
Controlled value for the autocomplete input (requires autocomplete to be true).
onInputValueChange
(value: string) => void
Callback fired when the autocomplete input value changes.
defaultInputValue
string
The initial input value in uncontrolled mode.
A button that opens the menu when clicked.
asChild
boolean
When true, merges props with the immediate child element instead of rendering a button.
stopPropagation
boolean
default:"true"
Whether to stop click event propagation.
The menu content container with positioning logic.
side
'top' | 'right' | 'bottom' | 'left'
default:"'bottom'"
The preferred side of the trigger to render the menu.
align
'start' | 'center' | 'end'
default:"'start'"
The preferred alignment against the trigger.
sideOffset
number
default:"4"
The distance in pixels from the trigger.
searchPlaceholder
string
default:"'Search...'"
Placeholder text for the autocomplete input (when autocomplete is enabled).
className
string
Additional CSS classes for the menu content.
An individual menu item that can be selected.
onSelect
() => void
Callback fired when the item is selected.
disabled
boolean
Whether the item is disabled.
value
string
The value used for autocomplete filtering. If not provided, uses the text content.
leadingIcon
React.ReactNode
Icon or element displayed before the item text.
trailingIcon
React.ReactNode
Icon or element displayed after the item text.
A container for grouping related menu items. A label for a group of menu items. A visual separator between menu items or groups. A component to display when no menu items match the search (useful with autocomplete). A nested submenu component with its own state and autocomplete support.
autocomplete
boolean
Enable autocomplete mode for the submenu.
autocompleteMode
'auto' | 'manual'
default:"'auto'"
Autocomplete filtering mode for the submenu.
A menu item that opens a submenu. Automatically shows a right arrow icon.
value
string
The value used for autocomplete filtering.
leadingIcon
React.ReactNode
Icon or element displayed before the item text.
trailingIcon
React.ReactNode
Icon or element displayed after the item text. Defaults to a right arrow.
The content container for a submenu. Has the same props as Menu.Content but with sideOffset defaulting to 2.

Usage examples

<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Group>
      <Menu.Label>Group 1</Menu.Label>
      <Menu.Item>Item 1</Menu.Item>
      <Menu.Item>Item 2</Menu.Item>
    </Menu.Group>
    
    <Menu.Separator />
    
    <Menu.Group>
      <Menu.Label>Group 2</Menu.Label>
      <Menu.Item>Item 3</Menu.Item>
      <Menu.Item>Item 4</Menu.Item>
    </Menu.Group>
  </Menu.Content>
</Menu>
import { PlusIcon, TrashIcon } from '@radix-ui/react-icons';

<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Item leadingIcon={<PlusIcon />}>New item</Menu.Item>
    <Menu.Item leadingIcon={<TrashIcon />}>Delete</Menu.Item>
  </Menu.Content>
</Menu>

Nested submenu

<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Item>Regular item</Menu.Item>
    
    <Menu.Submenu>
      <Menu.SubmenuTrigger>More options</Menu.SubmenuTrigger>
      <Menu.SubmenuContent>
        <Menu.Item>Submenu item 1</Menu.Item>
        <Menu.Item>Submenu item 2</Menu.Item>
      </Menu.SubmenuContent>
    </Menu.Submenu>
  </Menu.Content>
</Menu>
<Menu autocomplete>
  <Menu.Trigger asChild>
    <button>Open searchable menu</button>
  </Menu.Trigger>
  
  <Menu.Content searchPlaceholder="Search items...">
    <Menu.Item value="apple">Apple</Menu.Item>
    <Menu.Item value="banana">Banana</Menu.Item>
    <Menu.Item value="cherry">Cherry</Menu.Item>
    <Menu.Item value="date">Date</Menu.Item>
    <Menu.EmptyState>No items found</Menu.EmptyState>
  </Menu.Content>
</Menu>

Controlled autocomplete

function ControlledMenu() {
  const [search, setSearch] = useState('');
  
  return (
    <Menu
      autocomplete
      inputValue={search}
      onInputValueChange={setSearch}
    >
      <Menu.Trigger asChild>
        <button>Open menu</button>
      </Menu.Trigger>
      
      <Menu.Content>
        <Menu.Item value="item1">Item 1</Menu.Item>
        <Menu.Item value="item2">Item 2</Menu.Item>
        <Menu.EmptyState>No results for "{search}"</Menu.EmptyState>
      </Menu.Content>
    </Menu>
  );
}
<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Item>Regular item</Menu.Item>
    
    <Menu.Submenu autocomplete>
      <Menu.SubmenuTrigger>Search options</Menu.SubmenuTrigger>
      <Menu.SubmenuContent>
        <Menu.Item value="option1">Option 1</Menu.Item>
        <Menu.Item value="option2">Option 2</Menu.Item>
        <Menu.Item value="option3">Option 3</Menu.Item>
        <Menu.EmptyState>No matches</Menu.EmptyState>
      </Menu.SubmenuContent>
    </Menu.Submenu>
  </Menu.Content>
</Menu>

Disabled items

<Menu>
  <Menu.Trigger asChild>
    <button>Open menu</button>
  </Menu.Trigger>
  
  <Menu.Content>
    <Menu.Item>Enabled item</Menu.Item>
    <Menu.Item disabled>Disabled item</Menu.Item>
    <Menu.Item>Another enabled item</Menu.Item>
  </Menu.Content>
</Menu>

Autocomplete filtering

The Menu component supports two autocomplete modes:

Auto mode (default)

Automatically filters menu items based on the search input. Items that don’t match are hidden:
<Menu autocomplete autocompleteMode="auto">
  {/* Menu content */}
</Menu>
Filtering logic:
  • Matches against the value prop if provided
  • Falls back to matching against the item’s text content
  • Case-insensitive matching
  • Shows Menu.EmptyState when no items match

Manual mode

Provides the search input but doesn’t filter items automatically. You control the filtering logic:
function ManualFilterMenu() {
  const [search, setSearch] = useState('');
  const items = ['Apple', 'Banana', 'Cherry'];
  
  const filteredItems = items.filter(item =>
    item.toLowerCase().includes(search.toLowerCase())
  );
  
  return (
    <Menu
      autocomplete
      autocompleteMode="manual"
      inputValue={search}
      onInputValueChange={setSearch}
    >
      <Menu.Trigger asChild>
        <button>Open menu</button>
      </Menu.Trigger>
      
      <Menu.Content>
        {filteredItems.map(item => (
          <Menu.Item key={item}>{item}</Menu.Item>
        ))}
        {filteredItems.length === 0 && (
          <Menu.EmptyState>No results</Menu.EmptyState>
        )}
      </Menu.Content>
    </Menu>
  );
}

Accessibility features

  • Keyboard navigation: Arrow keys to navigate, Enter/Space to select
  • Type-ahead: Start typing to highlight matching items (when autocomplete is disabled)
  • ESC to close: Press Escape to close the menu
  • Focus management: Automatic focus handling between menu and submenus
  • Arrow key submenu navigation: Right arrow opens submenu, left arrow closes it
  • ARIA attributes: Proper ARIA roles and relationships
  • Focus trap: Focus stays within the menu when open

Trigger patterns

The Menu.Trigger component can be used in two ways:
  1. Default button: Renders a button when no children are provided or asChild is false
  2. Custom trigger: Use asChild prop to merge menu functionality with your component
{/* Default button */}
<Menu.Trigger>Open menu</Menu.Trigger>

{/* Custom trigger */}
<Menu.Trigger asChild>
  <button className="custom-button">Open menu</button>
</Menu.Trigger>