# Dropdown Menu

> A React dropdown menu: icons, descriptions, shortcuts, badges, checkbox and radio items, submenus, and an action sheet on phones.

- Type: React component (Overlays)
- Access: Free (the source is open to everyone)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add dropdown-menu`
- Docs: https://wingo-ui.com/components/dropdown-menu

## Interaction

Click the trigger and the menu grows out of it on a quick spring. Hover or use the arrow keys to move the highlight (it wraps around), type a letter to jump to an item, Right opens a submenu and Left closes it, Enter or Space picks, Escape closes and focus returns to the trigger. The shortcuts written next to items run them while the menu is open, and an optional shortcut opens the menu from anywhere. Checkbox and radio items tick in place (the check draws itself) and keep the menu open; danger items turn red. On a phone the same menu slides up as an action sheet: 48px rows, the title centered on top, a Cancel button where the thumb is, and drag down to dismiss. A submenu row slides a new page in from the right with a Back row while the sheet resizes to fit. Opened from the keyboard, the sheet focuses its first row; the arrow keys move between rows, Right opens a submenu page and Left goes back. With reduced motion the menu and the pages fade instead of moving.

## Installation

```bash
npx wingo-ui@latest add dropdown-menu
```

The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add https://wingo-ui.com/r/dropdown-menu.json`

Files:

- `components/ui/dropdown-menu.tsx`

npm dependencies: `npm i @radix-ui/react-dropdown-menu lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Copy, Ellipsis, Pencil, Share2, Trash2 } from "lucide-react"
import { Button } from "@/components/ui/button"
import { DropdownMenu, type MenuEntry } from "@/components/ui/dropdown-menu"

export function ListingActions({ onEdit, onDelete }: { onEdit: () => void; onDelete: () => void }) {
  const items: MenuEntry[] = [
    { label: "Edit", icon: <Pencil />, shortcut: "mod+e", onSelect: onEdit },
    { label: "Duplicate", icon: <Copy />, shortcut: "mod+d" },
    {
      type: "submenu",
      label: "Share",
      icon: <Share2 />,
      items: [{ label: "WhatsApp" }, { label: "Copy link", shortcut: "mod+c" }],
    },
    { type: "separator" },
    // a confirm dialog waits a frame, so focus is back on the trigger when it opens
    { label: "Delete", icon: <Trash2 />, tone: "danger", onSelect: () => requestAnimationFrame(onDelete) },
  ]

  return (
    <DropdownMenu
      align="end"
      items={items}
      title="Pegas city bike"
      description="$420 · Brooklyn"
      trigger={
        <Button variant="ghost" size="icon" radius="full" aria-label="Listing actions">
          <Ellipsis />
        </Button>
      }
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen?` | `boolean` | `false` | Starts open (uncontrolled). Pass open and onOpenChange instead to control it. |
| `open?` | `boolean` |  | Controlled open state. The root keeps it across the 768px breakpoint, so resizing never closes the menu. |
| `onOpenChange?` | `(open: boolean) => void` |  | Called when the menu opens or closes: the trigger, an item, Escape, an outside click, the sheet's Cancel or a drag. |
| `items?` | `MenuEntry[]` |  | The entries: items ({ label, icon, description, shortcut, badge, onSelect, href, tone: "danger", keepOpen }), { type: "checkbox", checked, onCheckedChange }, { type: "radio", value, onValueChange, options }, { type: "submenu", items }, { type: "group", label, items }, { type: "label" } and { type: "separator" }. Groups and radio sets get separators around them on their own. An onSelect that opens a dialog should wait a frame (requestAnimationFrame) so focus returns to the trigger first. With no enabled entry the trigger is disabled. |
| `trigger?` | `ReactNode` |  | The element that opens it, rendered with asChild: a single element that takes a ref (a Button, an Avatar wrapped in a button). It gets aria-keyshortcuts when a shortcut is set. |
| `children?` | `ReactNode` |  | Extra rows after the items, built from the parts (DropdownMenuItem and friends). They render in the sheet too. |
| `title?` | `ReactNode` | `""` | The action sheet's header on phones, centered and small like iOS (usually what the actions apply to). Not shown on desktop. The demo shows its sample while this is empty. |
| `description?` | `ReactNode` | `""` | A quieter line under the sheet title, linked with aria-describedby. The demo shows its sample while this is empty. |
| `sheetTitle?` | `string` | `"Actions"` | The sheet's title for screen readers when there is no visible title. |
| `showCancel?` | `boolean` | `true` | The sheet's Cancel button at the bottom, where the thumb is. The sheet still closes on a drag, a tap outside or Escape without it. |
| `cancelLabel?` | `string` | `"Cancel"` | The label of the sheet's Cancel button. |
| `backLabel?` | `string` | `"Back"` | The back row at the top of a submenu page in the sheet. |
| `size?` | `"sm" \| "md"` | `"md"` | Item height 32 / 36px with 13 / 14px text and 14 / 16px icons. 44px on touch either way; sheet rows are always 48px. |
| `radius?` | `"none" \| "sm" \| "md" \| "lg" \| "full"` | `"md"` | Panel corners 0 / 12 / 16 / 24px (full falls back to lg). Items keep the panel radius minus its 6px padding, so lg rows are pills. |
| `minWidth?` | `number` | `192` | The panel's minimum width in px. It grows for longer labels up to 384px (and never past the screen), then labels truncate. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | The side of the trigger it opens on. It flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | How it lines up with the trigger. end suits a ⋯ button at the right edge of a row. |
| `sideOffset?` | `number` | `8` | The gap between the trigger and the panel, in px. |
| `loop?` | `boolean` | `true` | Arrow keys wrap from the last item to the first (desktop and sheet). The demo says which it does. |
| `modal?` | `boolean` | `true` | While open, the page behind ignores the pointer and does not scroll (Radix). The sheet on phones is always modal. |
| `responsive?` | `boolean` | `true` | Below 768px the menu becomes an action sheet with large rows and submenu pages. Turn it off to keep the floating menu. |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation (⌘ on Apple, Ctrl elsewhere) that opens and closes the menu from anywhere on the page. Empty binds nothing. |
| `onOpenAutoFocus?` | `(event: Event) => void` |  | Sheet only: runs when the action sheet takes focus. Opened with the keyboard it focuses the first row, opened by a tap the sheet itself; preventDefault to focus something else. The desktop menu focuses its first item on a key open (Radix). |
| `container?` | `HTMLElement \| null` |  | Portal target for the menu and the sheet (default document.body). |
| `classNames?` | `Partial<Record<"content" \| "item" \| "icon" \| "label" \| "description" \| "shortcut" \| "separator" \| "sheet", string>>` |  | Extra classes for the parts: the panel (and sub panels), every row, row icons, labels, descriptions, shortcuts, separators, and the sheet. |
| `className?` | `string` |  | Extra classes for the panel (or the sheet), merged last. Other Radix content props (onCloseAutoFocus, alignOffset, avoidCollisions…) pass through. |

## Parts

### DropdownMenuRoot (component)

Compound API: holds the state and decides the mode once (the floating Radix menu, or the action sheet below 768px). Every part below reads it and renders for that mode, so one tree works on desktop and on phones. Props: { open, defaultOpen, onOpenChange, modal, responsive, shortcut, container, dir, children }.

### DropdownMenuTrigger (component)

Opens the menu; with asChild any element that takes a ref. It carries aria-keyshortcuts when the root has a shortcut. Props: Radix DropdownMenu.Trigger props (asChild).

### DropdownMenuContent (component)

The panel (dropdown-menu-content with data-size, data-radius, data-side) with the popover spring, or the sheet on phones. Renders items first, then JSX children, and runs item shortcuts while open. Props: { items, size, radius, minWidth, title, description, sheetTitle, showCancel, cancelLabel, backLabel, container, classNames } & Radix DropdownMenu.Content props.

### DropdownMenuItem (component)

A row. href renders an <a>; asChild takes your router Link and keeps the row layout around its text (a Link that prevents the click still closes the menu). In the sheet it is a 48px button. Props: { icon, description, shortcut, badge, tone: "neutral" | "danger", keepOpen, href, target, rel, asChild, disabled, onSelect }.

### DropdownMenuCheckboxItem (component)

A row with a leading checkbox whose check draws itself; it stays open while toggling unless keepOpen is false. Props: { checked, defaultChecked, onCheckedChange, icon, description, shortcut, keepOpen = true, disabled } ("indeterminate" allowed).

### DropdownMenuRadioGroup (component)

Holds one choice from a set of radio items. Props: { value, defaultValue, onValueChange }.

### DropdownMenuRadioItem (component)

One choice in a radio group, marked with a trailing check. Pass keepOpen={false} for a sort menu that closes on pick. Props: { value, icon, description, keepOpen = true, disabled }.

### DropdownMenuSub (component)

A nested menu: holds the open state of the side panel on desktop, a pushed page with a Back row in the sheet. Props: { open, defaultOpen, onOpenChange }.

### DropdownMenuSubTrigger (component)

The row with a chevron that opens the nested menu. Props: { icon, description, badge, disabled }.

### DropdownMenuSubContent (component)

The side panel (or sheet page) of a nested menu. Props: { items, sideOffset, alignOffset, container }.

### DropdownMenuGroup (component)

Groups related items into a section. Takes Radix Group props.

### DropdownMenuLabel (component)

The small muted label of a section. Takes Radix Label props.

### DropdownMenuSeparator (component)

A hairline between sections. Takes Radix Separator props.

### MenuSheet (component)

The action sheet on its own, with its own drawer root: open it from a long press or a split button (context-menu and button-group use it on phones). Props: { open, defaultOpen, onOpenChange, items, title, description, sheetTitle, showCancel, cancelLabel, backLabel, loop, container, classNames, className, children, onCloseAutoFocus }.

### MenuItemContent (component)

The inside of a row (checkbox, icon, label and description, badge, shortcut, chevron or radio check) for any Radix menu item; slot sets the data-slot prefix, e.g. context-menu. Props: { label, icon, description, badge, shortcut, submenu, indicator: "checkbox" | "radio", checked, size, sheet, tone, slot, classNames }.

### MENU_CLASSES (constant)

The shared class recipe (content, item, itemWithDescription, icon, label, description, shortcut, badge, groupLabel, separator, subChevron, check, sheetItem, sheetSeparator...), so context menus and split buttons match.

### menuItemClass (function)

menuItemClass({ size, radius, tone, description }): the desktop row class for a size, radius and tone. Context menus build their rows with it.

### menuSheetItemClass (function)

menuSheetItemClass({ tone, description }): the bottom sheet row class, 48px tall or 56px with a description.

### MENU_ITEM_SIZES (constant)

The row classes per size: sm is 32px and md 36px, both 44px on a coarse pointer, with matching gap, padding, type and icon size.

### MENU_RADII (constant)

The panel and row corners per radius; rows take the panel radius minus its 6px padding so the shapes stay concentric.

### MenuEntry (type)

One entry of items: an item, checkbox, radio group, submenu, group, label or separator, told apart by type.

### DropdownMenuProps (type)

The props of DropdownMenu, for wrappers that build menus from your own data.

## Examples

- **User menu**: An Avatar trigger, a count badge, a theme radio group and shortcuts, with the name as the sheet title on phones.
- **Sort menu**: A radio group with keepOpen off: picking an option closes the menu and renames the button.
- **Column visibility**: Checkbox items stay open while you toggle; the table follows along.
- **Nested submenus**: Two levels deep. Right and Left move between levels; on a phone each level is a page.
- **Sizes and radii**: size sm or md, and every radius; the items follow the panel's corners.
- **Parts with links**: The parts API with asChild anchors, as with a router Link: navigation runs and the menu closes.
- **Action sheet on a phone**: MenuSheet in a phone frame: 48px rows, a Cancel button at the thumb and submenu pages.

## See also

- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))

---

Canonical: https://wingo-ui.com/components/dropdown-menu
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
