# Dropdown Menu

A short list of actions or options that opens from a trigger.

```tsx
import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  toast,
} from 'ferry-ui'
import { Archive, ChevronDown, Copy, Pencil, Share2 } from 'lucide-react'

export default function DropdownMenuHero() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button iconRight={<ChevronDown />}>Actions</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel>Project</DropdownMenuLabel>
        <DropdownMenuItem onSelect={() => toast('Edit the project')}>
          <Pencil /> Edit
        </DropdownMenuItem>
        <DropdownMenuItem onSelect={() => toast.success('Project duplicated')}>
          <Copy /> Duplicate
        </DropdownMenuItem>
        <DropdownMenuItem onSelect={() => toast('Share the project')}>
          <Share2 /> Share
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem onSelect={() => toast.success('Project archived')}>
          <Archive /> Archive
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

## Usage guidelines

- **A list of actions.** Use a menu for the actions of a row or a card, and for view options.
- **A form value is not a menu.** To pick the value of a field, use [Select](/docs/components/select).
- **Other content needs a popover.** For a small form or a picker, use [Popover](/docs/components/popover).
- **Confirm a destructive item.** A `destructive` item opens a [Confirm Dialog](/docs/components/confirm-dialog). It does not delete on a click.

## Anatomy

Import the parts. Put them together in this order.

```tsx title="Anatomy"

  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuPortal,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
} from 'ferry-ui'

<DropdownMenu>
  <DropdownMenuTrigger />
  <DropdownMenuContent>
    <DropdownMenuLabel />
    <DropdownMenuGroup>
      <DropdownMenuItem>
        <DropdownMenuShortcut />
      </DropdownMenuItem>
    </DropdownMenuGroup>
    <DropdownMenuSeparator />
    <DropdownMenuCheckboxItem />
    <DropdownMenuRadioGroup>
      <DropdownMenuRadioItem />
    </DropdownMenuRadioGroup>
    <DropdownMenuSub>
      <DropdownMenuSubTrigger />
      <DropdownMenuPortal>
        <DropdownMenuSubContent />
      </DropdownMenuPortal>
    </DropdownMenuSub>
  </DropdownMenuContent>
</DropdownMenu>
```

| Part | Role |
| --- | --- |
| `DropdownMenu` | Holds the open state. |
| `DropdownMenuTrigger` | Opens the menu. |
| `DropdownMenuContent` | The panel. |
| `DropdownMenuItem` | One action. |
| `DropdownMenuCheckboxItem` | An option that is on or off. |
| `DropdownMenuRadioGroup` | Holds the radio items and their value. |
| `DropdownMenuRadioItem` | One option of a radio group. |
| `DropdownMenuLabel` | A heading above a group of items. |
| `DropdownMenuSeparator` | A line between two groups. |
| `DropdownMenuShortcut` | A key hint at the right of an item. It binds no key. |
| `DropdownMenuGroup` | Groups items for screen readers. It has no style. |
| `DropdownMenuSub` | Holds a submenu. |
| `DropdownMenuSubTrigger` | The item that opens the submenu. |
| `DropdownMenuSubContent` | The panel of the submenu. |
| `DropdownMenuPortal` | Renders the panel of a submenu in `document.body`. |

With `asChild`, `DropdownMenuTrigger` gives its behavior to its child element. Use it with a [Button](/docs/components/button). With `asChild`, `DropdownMenuItem` renders its child element, for example a link.

## Examples

### Items

Run the action of an item in `onSelect`. An item can hold an icon and a `DropdownMenuShortcut`. Set `disabled` on an action that is not available.

```tsx
import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuGroup,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
  toast,
  useModKey,
} from 'ferry-ui'
import { Copy, Download, MoreHorizontal, Send } from 'lucide-react'

export default function DropdownMenuItems() {
  const mod = useModKey()
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon" icon={<MoreHorizontal />} aria-label="Actions for INV-2041" />
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-56">
        <DropdownMenuLabel>Invoice INV-2041</DropdownMenuLabel>
        <DropdownMenuGroup>
          <DropdownMenuItem onSelect={() => toast.success('Invoice duplicated')}>
            <Copy /> Duplicate
            <DropdownMenuShortcut>{mod} D</DropdownMenuShortcut>
          </DropdownMenuItem>
          <DropdownMenuItem onSelect={() => toast('The download starts')}>
            <Download /> Download PDF
          </DropdownMenuItem>
          {/* The customer paid this invoice: the action is not available. */}
          <DropdownMenuItem disabled>
            <Send /> Send a reminder
          </DropdownMenuItem>
        </DropdownMenuGroup>
        <DropdownMenuSeparator />
        <DropdownMenuItem onSelect={() => toast('Open the customer')}>View customer</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Checkbox items

`DropdownMenuCheckboxItem` takes `checked` and `onCheckedChange`. To keep the menu open after a click, call `event.preventDefault()` in `onSelect`.

The `inset` prop aligns a label or a plain item with the checkbox items.

```tsx
import * as React from 'react'
import {
  Button,
  DropdownMenu,
  DropdownMenuCheckboxItem,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from 'ferry-ui'
import { Settings2 } from 'lucide-react'

const DEFAULT_COLUMNS = { email: true, role: true, status: false }

export default function DropdownMenuCheckboxItems() {
  const [columns, setColumns] = React.useState(DEFAULT_COLUMNS)

  function toggle(key: keyof typeof DEFAULT_COLUMNS) {
    return (checked: boolean) => setColumns((current) => ({ ...current, [key]: checked }))
  }

  // `preventDefault` keeps the menu open, so the user can change more than one column.
  const keepOpen = (event: Event) => event.preventDefault()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button icon={<Settings2 />}>View</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel inset>Columns</DropdownMenuLabel>
        <DropdownMenuCheckboxItem checked disabled>
          Name
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem checked={columns.email} onCheckedChange={toggle('email')} onSelect={keepOpen}>
          Email
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem checked={columns.role} onCheckedChange={toggle('role')} onSelect={keepOpen}>
          Role
        </DropdownMenuCheckboxItem>
        <DropdownMenuCheckboxItem checked={columns.status} onCheckedChange={toggle('status')} onSelect={keepOpen}>
          Status
        </DropdownMenuCheckboxItem>
        <DropdownMenuSeparator />
        {/* `inset` aligns the text of a plain item with the checkbox items. */}
        <DropdownMenuItem inset onSelect={() => setColumns(DEFAULT_COLUMNS)}>
          Reset columns
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Radio items

`DropdownMenuRadioGroup` holds one value. Pass `value` and `onValueChange`.

```tsx
import * as React from 'react'
import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuLabel,
  DropdownMenuRadioGroup,
  DropdownMenuRadioItem,
  DropdownMenuTrigger,
} from 'ferry-ui'
import { ArrowUpDown } from 'lucide-react'

const LABELS: Record<string, string> = {
  newest: 'Newest first',
  oldest: 'Oldest first',
  name: 'Name (A to Z)',
}

export default function DropdownMenuRadioItems() {
  const [sort, setSort] = React.useState('newest')

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button icon={<ArrowUpDown />}>Sort: {LABELS[sort]}</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuLabel inset>Sort by</DropdownMenuLabel>
        <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
          <DropdownMenuRadioItem value="newest">Newest first</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="oldest">Oldest first</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="name">Name (A to Z)</DropdownMenuRadioItem>
          <DropdownMenuRadioItem value="amount" disabled>
            Amount
          </DropdownMenuRadioItem>
        </DropdownMenuRadioGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Submenu

Put `DropdownMenuSubTrigger` and `DropdownMenuSubContent` in `DropdownMenuSub`. Wrap the content in `DropdownMenuPortal`. Use one level of submenu only.

```tsx
import {
  Button,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuPortal,
  DropdownMenuSub,
  DropdownMenuSubContent,
  DropdownMenuSubTrigger,
  DropdownMenuTrigger,
  toast,
} from 'ferry-ui'
import { ChevronDown, FolderInput, Pencil } from 'lucide-react'

const TEAMS = ['Marketing', 'Product', 'Finance']

export default function DropdownMenuSubmenu() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button iconRight={<ChevronDown />}>Document</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-48">
        <DropdownMenuItem onSelect={() => toast('Rename the document')}>
          <Pencil /> Rename
        </DropdownMenuItem>
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>
            <FolderInput /> Move to
          </DropdownMenuSubTrigger>
          {/* The portal makes sure that the first panel does not cut the submenu. */}
          <DropdownMenuPortal>
            <DropdownMenuSubContent className="w-40">
              {TEAMS.map((team) => (
                <DropdownMenuItem key={team} onSelect={() => toast.success(`Document moved to ${team}`)}>
                  {team}
                </DropdownMenuItem>
              ))}
            </DropdownMenuSubContent>
          </DropdownMenuPortal>
        </DropdownMenuSub>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Side and alignment

The `align` prop of `DropdownMenuContent` aligns the panel to the start, the center or the end of the trigger. The `side` prop sets the side where the panel opens. Set the width with a class such as `w-48`.

```tsx
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from 'ferry-ui'

const ALIGNMENTS = ['start', 'center', 'end'] as const

export default function DropdownMenuAlign() {
  return (
    <>
      {ALIGNMENTS.map((align) => (
        <DropdownMenu key={align}>
          <DropdownMenuTrigger asChild>
            <Button>{align}</Button>
          </DropdownMenuTrigger>
          <DropdownMenuContent align={align} className="w-48">
            <DropdownMenuItem>Open</DropdownMenuItem>
            <DropdownMenuItem>Duplicate</DropdownMenuItem>
            <DropdownMenuItem>Archive</DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      ))}
    </>
  )
}
```

### Open state

A menu holds its open state by default. To control the state, pass `open` and `onOpenChange`.

Pass `modal={false}` to keep the page interactive while the menu is open.

```tsx
import * as React from 'react'
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, toast } from 'ferry-ui'
import { ChevronDown, ChevronUp } from 'lucide-react'

export default function DropdownMenuControlled() {
  const [open, setOpen] = React.useState(false)

  return (
    <DropdownMenu open={open} onOpenChange={setOpen}>
      <DropdownMenuTrigger asChild>
        {/* The trigger reads the state: the arrow changes while the menu is open. */}
        <Button iconRight={open ? <ChevronUp /> : <ChevronDown />}>Export</Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="start" className="w-40">
        <DropdownMenuItem onSelect={() => toast.success('CSV export ready')}>CSV file</DropdownMenuItem>
        <DropdownMenuItem onSelect={() => toast.success('PDF export ready')}>PDF file</DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}
```

### Destructive item

Give `variant="destructive"` to an item that deletes or revokes. The item opens a Confirm Dialog with a controlled `open`.

```tsx
import * as React from 'react'
import {
  Button,
  ConfirmDialog,
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
  toast,
} from 'ferry-ui'
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'

export default function DropdownMenuConfirm() {
  const [confirmOpen, setConfirmOpen] = React.useState(false)

  return (
    <>
      <span className="font-mono text-[13px] text-foreground">Analytics export</span>
      <DropdownMenu>
        <DropdownMenuTrigger asChild>
          <Button
            variant="ghost"
            size="icon-tiny"
            icon={<MoreHorizontal />}
            aria-label="Actions for the API key Analytics export"
          />
        </DropdownMenuTrigger>
        <DropdownMenuContent align="end">
          <DropdownMenuItem onSelect={() => toast('Rename the API key')}>
            <Pencil /> Rename
          </DropdownMenuItem>
          <DropdownMenuSeparator />
          {/* The item opens the confirmation. It does not revoke the key on a click. */}
          <DropdownMenuItem variant="destructive" onSelect={() => setConfirmOpen(true)}>
            <Trash2 /> Revoke key
          </DropdownMenuItem>
        </DropdownMenuContent>
      </DropdownMenu>
      <ConfirmDialog
        open={confirmOpen}
        onOpenChange={setConfirmOpen}
        title="Revoke the API key “Analytics export”?"
        description="Requests with this key fail from now on. This cannot be undone."
        confirmLabel="Revoke key"
        onConfirm={() => {
          toast.success('API key revoked')
        }}
      />
    </>
  )
}
```

## Accessibility

- Give an icon-only trigger an `aria-label` that names the target, for example "Actions for INV-2041".
- Run each action in `onSelect`, not in `onClick`. Then the keyboard selection works.
- <Kbd>Enter</Kbd>, <Kbd>Space</Kbd> or <Kbd>↓</Kbd> on the trigger opens the menu.
- The arrow keys move the focus between the items. <Kbd>Enter</Kbd> selects an item. <Kbd>→</Kbd> opens a submenu.

## API reference

Each part also accepts the props of its Radix UI primitive and the attributes of its element.

### DropdownMenu

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `dir` | `"ltr" \| "rtl"` |  |  |
| `open` | `boolean` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  |  |
| `modal` | `boolean` |  |  |

### DropdownMenuTrigger

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

### DropdownMenuContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` |  |  |
| `onEscapeKeyDown` | `((event: KeyboardEvent) => void)` |  |  |
| `onPointerDownOutside` | `((event: PointerDownOutsideEvent) => void)` |  |  |
| `onFocusOutside` | `((event: FocusOutsideEvent) => void)` |  |  |
| `onInteractOutside` | `((event: FocusOutsideEvent \| PointerDownOutsideEvent) => void)` |  |  |
| `onCloseAutoFocus` | `((event: Event) => void)` |  | Event handler called when auto-focusing on close. Can be prevented. |
| `forceMount` | `true` |  | Used to force mounting when more control is needed. Useful when controlling animation with React animation libraries. |
| `loop` | `boolean` | `false` | Whether keyboard navigation should loop around |
| `side` | `"top" \| "right" \| "bottom" \| "left"` |  |  |
| `sideOffset` | `number` | `4` |  |
| `align` | `"center" \| "start" \| "end"` |  |  |
| `alignOffset` | `number` |  |  |
| `arrowPadding` | `number` |  |  |
| `avoidCollisions` | `boolean` |  |  |
| `collisionBoundary` | `Boundary \| Boundary[]` |  |  |
| `collisionPadding` | `number \| Partial<Record<"top" \| "right" \| "bottom" \| "left", number>>` |  |  |
| `sticky` | `"partial" \| "always"` |  |  |
| `hideWhenDetached` | `boolean` |  |  |
| `updatePositionStrategy` | `"always" \| "optimized"` |  |  |

### DropdownMenuItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` |  | Adds left padding so the text lines up with checkbox / radio items and inset labels. |
| `variant` | `"default" \| "destructive"` | `default` | `destructive` colors the item for irreversible actions (delete, remove, revoke). |
| `disabled` | `boolean` |  |  |
| `onSelect` | `((event: Event) => void)` |  |  |
| `asChild` | `boolean` |  |  |
| `textValue` | `string` |  |  |

### DropdownMenuCheckboxItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` |  |  |
| `onSelect` | `((event: Event) => void)` |  |  |
| `asChild` | `boolean` |  |  |
| `checked` | `"indeterminate" \| boolean` |  |  |
| `textValue` | `string` |  |  |
| `onCheckedChange` | `((checked: boolean) => void)` |  |  |

### DropdownMenuRadioGroup

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` |  |  |
| `asChild` | `boolean` |  |  |
| `onValueChange` | `((value: string) => void)` |  |  |

### DropdownMenuRadioItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `string` |  |  |
| `disabled` | `boolean` |  |  |
| `onSelect` | `((event: Event) => void)` |  |  |
| `asChild` | `boolean` |  |  |
| `textValue` | `string` |  |  |

### DropdownMenuLabel

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` |  | Adds left padding so the text lines up with checkbox / radio items. |
| `asChild` | `boolean` |  |  |

### DropdownMenuSeparator

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

### DropdownMenuShortcut

`DropdownMenuShortcut` has no props of its own. It accepts the attributes of the element it renders.

### DropdownMenuGroup

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

### DropdownMenuSub

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `open` | `boolean` |  |  |
| `defaultOpen` | `boolean` |  |  |
| `onOpenChange` | `((open: boolean) => void)` |  |  |

### DropdownMenuSubTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `inset` | `boolean` |  | Adds left padding so the text lines up with checkbox / radio items. |
| `disabled` | `boolean` |  |  |
| `asChild` | `boolean` |  |  |
| `textValue` | `string` |  |  |

### DropdownMenuSubContent

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

### DropdownMenuPortal

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` |  |  |
| `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. |
