# Dropdown menu

A list of actions that opens from a button, such as the actions of a table row.

- **Import:** `import { DropdownMenu, DropdownMenuPortal, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuLabel, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubTrigger, DropdownMenuSubContent } from "@booleanpress/ui/dropdown-menu"`
- **Radix Dropdown Menu:** <https://www.radix-ui.com/primitives/docs/components/dropdown-menu>
- **APG Menu Button:** <https://www.w3.org/WAI/ARIA/apg/patterns/menu-button/>
- **Page:** <https://ui.booleanpress.com/components/dropdown-menu> · @booleanpress/ui 0.1.0

## Usage

A dropdown menu is for commands, not navigation between options of a form (use a [select](/components/select) for that). The trigger is a button; choosing an item runs its `onSelect` and closes the menu.

```tsx
import { Button } from "@booleanpress/ui/button"
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@booleanpress/ui/dropdown-menu"

export function MailerActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Actions</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start">
        <DropdownMenuItem onSelect={() => duplicate()}>Duplicate</DropdownMenuItem>
        <DropdownMenuItem variant="destructive" onSelect={() => remove()}>Delete</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

`DropdownMenuCheckboxItem` toggles a setting and `DropdownMenuRadioItem` (inside `DropdownMenuRadioGroup`) chooses one of several; both stay in step with `checked`, or `value` and `onValueChange`. `DropdownMenuSub` nests a menu under a `DropdownMenuSubTrigger`. `DropdownMenuLabel`, `DropdownMenuSeparator` and `DropdownMenuGroup` organise items, and `DropdownMenuShortcut` shows a key hint without binding the key. An icon-only trigger needs `aria-label`. For a destructive action that cannot be undone, make the item open an [alert dialog](/components/alert-dialog).

## Examples

### Actions

A row's actions, with a label, a group and a disabled item.

```tsx
import { CopyIcon, MoreHorizontalIcon, PencilIcon, SendIcon } from "lucide-react"
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon" aria-label="Actions for Primary mailer">
          <MoreHorizontalIcon />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel>Primary mailer</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuGroup>
          <DropdownMenuItem>
            <PencilIcon />
            Edit
          </DropdownMenuItem>
          <DropdownMenuItem>
            <CopyIcon />
            Duplicate
          </DropdownMenuItem>
          <DropdownMenuItem>
            <SendIcon />
            Send test email
          </DropdownMenuItem>
          <DropdownMenuItem disabled>Move to another site</DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Checkbox items

Show or hide columns. Each choice closes the menu, unless `onSelect` calls `event.preventDefault()`.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuCheckboxItems() {
  const [columns, setColumns] = useState({ status: true, recipient: true, mailer: false })

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Columns</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel>Show columns</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuCheckboxItem
          checked={columns.status}
          onCheckedChange={(checked) => setColumns((c) => ({ ...c, status: checked === true }))}
        >
          Status
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={columns.recipient}
          onCheckedChange={(checked) => setColumns((c) => ({ ...c, recipient: checked === true }))}
        >
          Recipient
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem
          checked={columns.mailer}
          onCheckedChange={(checked) => setColumns((c) => ({ ...c, mailer: checked === true }))}
        >
          Mailer
        </DropdownMenuCheckboxItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Radio items

Choose one of several values, with the chosen one marked.

```tsx
import { useState } from "react"
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuRadioItems() {
  const [range, setRange] = useState("7d")

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Date range</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel>Show emails from</DropdownMenuLabel>
        <DropdownMenuSeparator />
        <DropdownMenuRadioGroup value={range} onValueChange={setRange}>
          <DropdownMenuRadioItem value="24h">The last 24 hours</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="7d">The last 7 days</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="30d">The last 30 days</DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Submenu

`DropdownMenuSub` opens a second list with → (← in right-to-left pages).

```tsx
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuSubmenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Export</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuItem>Export this page</DropdownMenuItem>
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>Export all as</DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            <DropdownMenuItem>CSV</DropdownMenuItem>
            <DropdownMenuItem>JSON</DropdownMenuItem>
            <DropdownMenuItem>Excel</DropdownMenuItem>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
        <DropdownMenuSeparator />
        <DropdownMenuItem>Schedule an export</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Destructive item

`variant="destructive"` colours an item that removes something.

```tsx
import { MoreHorizontalIcon, PencilIcon, TrashIcon } from "lucide-react"
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuDestructive() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon" aria-label="Actions for ticket 1042">
          <MoreHorizontalIcon />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-44">
        <DropdownMenuItem>
          <PencilIcon />
          Edit ticket
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive">
          <TrashIcon />
          Delete ticket
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### With shortcuts

`DropdownMenuShortcut` shows the key hint at the end of the row.

```tsx
import { Button } from "@booleanpress/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from "@booleanpress/ui/dropdown-menu"

export default function DropdownMenuShortcuts() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline">Ticket</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-56">
        <DropdownMenuItem>
          Reply
          <DropdownMenuShortcut>⌘R</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuItem>
          Add a note
          <DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem>
          Close ticket
          <DropdownMenuShortcut>⇧⌘C</DropdownMenuShortcut>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

## Accessibility

**Semantics.** The trigger is a `button` with `aria-haspopup="menu"`, `aria-expanded` and `aria-controls`. The content is `role="menu"`; items are `menuitem`, `menuitemcheckbox` (with `aria-checked`) or `menuitemradio`. A submenu trigger has `aria-haspopup="menu"` and `aria-expanded`.

**Labels.** The trigger's text names the menu; an icon-only trigger needs `aria-label`. `DropdownMenuLabel` names a section visually. Items are named by their text.

**Focus.** Focus moves to the menu when it opens (to the first item when opened with the keyboard) and returns to the trigger when it closes. The menu is modal: the page behind it cannot be reached.

**Known limits.**

- Only text and icons in items. A menu is not a place for form controls: use a popover.
- A disabled item stays in the list and is announced as unavailable; the arrow keys skip it.

### Keyboard

| Key | Behaviour |
| --- | --- |
| Enter + Space | On the trigger, opens the menu and highlights its first item. On an item, chooses it. |
| ↓ | On the trigger, opens the menu. In the menu, moves to the next item, wrapping at the end. |
| ↑ | In the menu, moves to the previous item, wrapping at the start. |
| Home + End | Moves to the first or last item. |
| A–Z | Type-ahead: moves to the next item whose text starts with the letters typed. |
| → | On a submenu trigger, opens its submenu (← in right-to-left pages). |
| ← | In a submenu, closes it and returns to its trigger (→ in right-to-left pages). |
| Escape | Closes the menu and returns focus to the trigger. |
| Tab | Does nothing while the menu is open: focus stays in the menu until it closes. |

## API

### DropdownMenu

Renders Radix DropdownMenu.Root and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  | Whether it starts open, when it controls itself. |
| `dir` | `"ltr" \| "rtl"` |  |  |
| `modal` | `boolean` |  | Whether the rest of the page is blocked while it is open. `true` by default. |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether it is open, when you control it. Pair it with `onOpenChange`. |

### DropdownMenuPortal

Renders Radix DropdownMenu.Portal and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `container` | `Element \| DocumentFragment \| null` |  | Specify a container element to portal the content into. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |

### DropdownMenuTrigger

Renders Radix DropdownMenu.Trigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |

### DropdownMenuContent

Renders Radix DropdownMenu.Content and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"center" \| "start" \| "end"` |  | Alignment along that side: `start`, `center` or `end`. `center` by default. |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"left" \| "right" \| "top" \| "bottom", number>>` |  |  |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `loop` | `boolean` |  | Whether keyboard navigation should loop around |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  | Called when Escape is pressed. Call `event.preventDefault()` to keep it open. |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  |  |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  |  |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  |  |
| `side` | `"left" \| "right" \| "top" \| "bottom"` |  | The preferred side: `top`, `right`, `bottom` or `left`. `bottom` by default; it flips when there is no room. |
| `sideOffset` | `number` | `4` | Distance in pixels from the trigger. 4 by default. |
| `sticky` | `"partial" \| "always"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### DropdownMenuGroup

Renders Radix DropdownMenu.Group and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### DropdownMenuLabel

Renders Radix DropdownMenu.Label and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `inset` | `boolean` |  | Indent the label to line up with items that have a check or dot. |

### DropdownMenuItem

Renders Radix DropdownMenu.Item and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  | Render the child element instead, with this part's behaviour and classes merged onto it. |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `inset` | `boolean` |  | Indent the item to line up with items that have a check or dot. |
| `onSelect` | `((event: Event) => void)` |  | Called when the item is chosen. Call `event.preventDefault()` to keep the menu open. |
| `textValue` | `string` |  | The text type-ahead matches, when the content is not plain text. |
| `variant` | `"default" \| "destructive"` | `default` | `default`, or `destructive` for an action that removes something. |

### DropdownMenuCheckboxItem

Renders Radix DropdownMenu.CheckboxItem and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `checked` | `boolean \| "indeterminate"` |  | Whether it is checked: `true`, `false` or `"indeterminate"`. Pair it with `onCheckedChange`. |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `onCheckedChange` | `((checked: boolean) => void)` |  | Called with the new state when it is chosen. |
| `onSelect` | `((event: Event) => void)` |  | Called when the item is chosen. Call `event.preventDefault()` to keep the menu open. |
| `textValue` | `string` |  |  |

### DropdownMenuRadioGroup

Renders Radix DropdownMenu.RadioGroup and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `onValueChange` | `((value: string) => void)` |  | Called with the value of the item that was chosen. |
| `value` | `string` |  | The chosen item's value. Pair it with `onValueChange`. |

### DropdownMenuRadioItem

Renders Radix DropdownMenu.RadioItem and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `onSelect` | `((event: Event) => void)` |  | Called when the item is chosen. Call `event.preventDefault()` to keep the menu open. |
| `textValue` | `string` |  |  |
| `value` | `string` |  | The value this item stands for. |

### DropdownMenuSeparator

Renders Radix DropdownMenu.Separator and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |

### DropdownMenuShortcut

Renders a `span` and passes it every other prop.

### DropdownMenuSub

Renders Radix DropdownMenu.Sub and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  | Called with `true` or `false` when it opens or closes. |
| `open` | `boolean` |  | Whether the submenu is open, when you control it. |

### DropdownMenuSubTrigger

Renders Radix DropdownMenu.SubTrigger and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `disabled` | `boolean` |  | Skips the item and ignores clicks. |
| `inset` | `boolean` |  | Indent the item to line up with items that have a check or dot. |
| `textValue` | `string` |  |  |

### DropdownMenuSubContent

Renders Radix DropdownMenu.SubContent and passes it every other prop.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `align` | `"start" \| "end"` |  | Controls the direction the subcontent appears from its anchor menu item Default: start |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `asChild` | `boolean` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"left" \| "right" \| "top" \| "bottom", number>>` |  |  |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `hideWhenDetached` | `boolean` |  |  |
| `loop` | `boolean` |  | Whether keyboard navigation should loop around |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  |  |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  |  |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  |  |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  |  |
| `sideOffset` | `number` | `4` | Distance in pixels from its trigger. |
| `sticky` | `"partial" \| "always"` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="dropdown-menu"` (DropdownMenu), `data-slot="dropdown-menu-portal"` (DropdownMenuPortal), `data-slot="dropdown-menu-trigger"` (DropdownMenuTrigger), `data-slot="dropdown-menu-content"` (DropdownMenuContent), `data-slot="dropdown-menu-group"` (DropdownMenuGroup), `data-slot="dropdown-menu-label"` (DropdownMenuLabel), `data-slot="dropdown-menu-item"` (DropdownMenuItem), `data-slot="dropdown-menu-checkbox-item"` (DropdownMenuCheckboxItem), `data-slot="dropdown-menu-radio-group"` (DropdownMenuRadioGroup), `data-slot="dropdown-menu-radio-item"` (DropdownMenuRadioItem), `data-slot="dropdown-menu-separator"` (DropdownMenuSeparator), `data-slot="dropdown-menu-shortcut"` (DropdownMenuShortcut), `data-slot="dropdown-menu-sub"` (DropdownMenuSub), `data-slot="dropdown-menu-sub-trigger"` (DropdownMenuSubTrigger), `data-slot="dropdown-menu-sub-content"` (DropdownMenuSubContent), and `data-inset`, `data-variant`.

## Theming

The menu is a floating surface: `--popover` and `--popover-foreground`. The highlighted item uses `--accent`; a destructive item uses `--destructive`.

| Token | Used for |
| --- | --- |
| `--accent` | background |
| `--accent-foreground` | text |
| `--border` | background |
| `--destructive` | text, background |
| `--muted-foreground` | text |
| `--popover` | background |
| `--popover-foreground` | text |
