Skip to content

ComponentsOverlays

Menu

A dropdown list of actions or options, opened from a button, a long press or a right click. Supports icons, shortcuts, sections, single and multiple selection, links, submenus and typeahead, with full keyboard navigation.

React AriaSource

Installation

pnpm dlx shadcn@latest add @desyne/menu

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import {
  MenuContent,
  MenuItem,
  MenuSection,
  MenuSeparator,
  MenuShortcut,
  MenuTrigger,
  SubmenuTrigger,
} from "@/components/ui/menu";
tsx
<MenuTrigger>
  <Button variant="outline">Actions</Button>
  <MenuContent onAction={(key) => run(key)}>
    <MenuItem id="edit">Edit</MenuItem>
    <MenuItem id="duplicate">Duplicate</MenuItem>
    <MenuSeparator />
    <MenuItem id="delete" variant="destructive">
      Delete
    </MenuItem>
  </MenuContent>
</MenuTrigger>

MenuTrigger connects the first child (any pressable, usually a Button) to the MenuContent after it. Handle actions with onAction on MenuContent, which receives the item's id, or with onAction on each MenuItem.

Set textValue when children aren't plain text

Typeahead and the item's text for assistive tech come from textValue. MenuItem fills it in when its children are a plain string; as soon as you add an icon or a shortcut, pass textValue yourself.

When to use

  • Menu — a list of actions ("Rename", "Delete") or view options ("Sort by", "Show columns") that don't need to be visible all the time.
  • Select — choosing a form value that's shown in the trigger and submitted.
  • Command Palette — a searchable list of every action in the app.
  • Toggle Button Group — a few options that should stay visible.
  • Popover — anything that isn't a list, like a small form.

Anatomy

tsx
<MenuTrigger>
  <Button />                     {/* trigger */}
  <MenuContent>                  {/* popover + role="menu" */}
    <MenuSection title="…">      {/* optional group with a heading */}
      <MenuItem>                 {/* role="menuitem" */}
        <Icon />
        Label
        <MenuShortcut />         {/* keyboard hint */}
      </MenuItem>
    </MenuSection>
    <MenuSeparator />
    <SubmenuTrigger>
      <MenuItem />               {/* opens the submenu */}
      <MenuContent />            {/* nested menu */}
    </SubmenuTrigger>
  </MenuContent>
</MenuTrigger>
PartRendersNotes
MenuTrigger—Opens the menu on press (or long press / right click) and wires up the trigger. From React Aria.
MenuContentPopover › <div role="menu">The positioned popover (min-w-44) and the scrollable menu inside it.
MenuItem<div role="menuitem">An action. Becomes menuitemradio / menuitemcheckbox with selection, and an <a> with href. Adds a selection indicator and a submenu chevron automatically.
MenuSection<section role="group">Groups items under an optional title. Can have its own selection.
MenuSeparator<hr>A 1px divider between items or sections.
MenuShortcut<kbd>Right-aligned shortcut hint, linked to the item for assistive tech.
SubmenuTrigger—Wraps a MenuItem and the nested MenuContent it opens. From React Aria.

Examples

Sections

MenuSection groups related items under a small uppercase title, and MenuSeparator divides groups. Icons in items are sized and muted automatically; MenuShortcut right-aligns a keyboard hint. The demo above shows all three.

Single selection

selectionMode="single" on MenuContent turns items into radio options with a dot indicator. Add disallowEmptySelection for settings like "Sort by" that always have a value, and reflect the choice in the trigger.

Selection in sections

Each MenuSection can have its own selectionMode, selectedKeys and onSelectionChange, mixing checkbox and radio groups in one menu. Multiple-selection menus stay open when an item is clicked, so several options can be toggled at once.

Rich items

Use React Aria's Text with slot="label" and slot="description" for two-line items. The slots wire up aria-labelledby and aria-describedby, so the description is announced separately from the name.

Dynamic items

Pass items and a render function instead of static children. Each item needs an id (or key) property; onAction receives it.

Wrap a MenuItem and a nested MenuContent in SubmenuTrigger. The item gets a chevron, opens on hover (after a 200ms delay), → or Enter, and closes with ←.

Give an item an href and it renders as a link, with target, rel and download passed through. It works with client-side routers through React Aria's RouterProvider.

Disabled items

disabledKeys on MenuContent (or isDisabled on a MenuItem) keeps an item visible but unfocusable. Explain why nearby when it isn't obvious.

Paid invoices are locked

Placement

The menu opens bottom start by default and flips when there isn't room. Use placement="bottom end" for triggers on the right edge of a layout, like a row's "more" button.

Context menu

trigger="contextMenu" opens the menu at the pointer on right click, on long press for touch, and with Shift+F10 on Windows. The trigger must be a focusable React Aria element, like a Button.

Recipes

Account menu

A user button that opens upward, sized to the trigger with popoverClassName="w-(--trigger-width)". A React Aria Header shows who's signed in, and a theme submenu uses single selection.

Row actions

A "more" menu on each row. "Delete…" opens a controlled alert Dialog after the menu closes, and the other actions confirm with a toast. The ellipsis in the label signals that the action asks for more input.

  • Production

    api.lumen.app

    Live
  • Staging

    staging.lumen.app

    Live
  • PR #482 preview

    pr-482.lumen.dev

    Paused

Column visibility

A multiple-selection menu that shows and hides table columns. It stays open while toggling, and the table updates live.

CustomerEmailPlanMRR
Acme Corpops@acme.comEnterprise$4,200
Globexit@globex.ioBusiness$890
Initechadmin@initech.coStarter$49

Accessibility

  • The trigger gets aria-haspopup="menu" and aria-expanded; the menu is labelled by its trigger unless you pass aria-label.
  • Opening with the keyboard focuses the first item (↑ focuses the last). Focus returns to the trigger on close.
  • Items are announced as menuitem, or menuitemradio / menuitemcheckbox with their checked state when the menu or section has a selectionMode.
  • Section titles label their group. MenuShortcut is linked to its item so the shortcut is announced.
  • Typeahead jumps to the next item that starts with the typed characters, using textValue.
  • Context menus don't get aria-haspopup, since they don't open on activation.

Keyboard

KeyAction
Space / Enter / ↓Opens the menu and focuses the first item
↑Opens the menu and focuses the last item
↓ / ↑Moves focus between items, wrapping at the ends
Home / EndFocuses the first / last item
EnterRuns the focused item and closes the menu
SpaceRuns or toggles the focused item; multiple-selection menus stay open
→ / ←Opens / closes a submenu (reversed in right-to-left locales)
EscCloses the menu (or the current submenu)
Any characterTypeahead to a matching item

Styling

Data attributes

On each MenuItem:

AttributePresent when
data-focusedThe item has focus (keyboard or hover)
data-focus-visibleThe item has keyboard focus
data-hoveredHovered with a mouse or pen
data-pressedBeing pressed
data-selectedThe item is selected
data-selection-modeThe menu or section has "single" or "multiple" selection
data-disabledThe item is disabled
data-has-submenuThe item opens a submenu
data-openThe item's submenu is open
data-variant"default" or "destructive"

MenuContent's menu has data-empty when there are no items; its popover has the popover attributes, including data-trigger="MenuTrigger" or "SubmenuTrigger".

Slots

data-slotElement
menuThe role="menu" element
menu-itemItems
menu-sectionSections
menu-separatorSeparators
menu-shortcutShortcut hints
popoverThe surrounding popover

Customizing

  • className on MenuContent styles the menu itself (width, padding); popoverClassName styles the popover around it, e.g. w-(--trigger-width) to match the trigger.
  • Icons inside items are size-4 and text-muted-foreground unless they have their own size-* or text-* class. Destructive items color their icons too.
  • The menu scrolls within the popover's available height.

Render props

MenuItem's className and children accept a function of its state:

tsx
<MenuItem className={({ isFocused }) => (isFocused ? "bg-brand/10" : "")}>
  {({ isSelected }) => (isSelected ? "Pinned" : "Pin")}
</MenuItem>

API Reference

Prop

Type

Prop

Type

Also accepts every prop of React Aria's Menu.

Prop

Type

Also accepts every prop of React Aria's MenuItem, including hover, focus and press handlers.

Prop

Type

Prop

Type

MenuSeparator accepts every prop of React Aria's Separator. MenuShortcut accepts every prop of React Aria's Keyboard (a <kbd>).

  • Command Palette — the same items, searchable in a modal.
  • Select — pick a form value from a list.
  • Popover — the surface menus render in.
  • Button — split buttons pair a button with a menu.