# DropdownMenu

> Action menu opened from a trigger, with items, checkbox and radio items, groups, shortcuts and submenus. Wraps Base UI Menu.

Source: https://docs.nasaqui.com/components/dropdown-menu

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/dropdown-menu.json
```

A menu of actions that opens from a trigger button. Built on Base UI `Menu`, so it has roving focus,
typeahead, submenus and full keyboard support. Use it to collect secondary actions on a row, card or header.

## When to use

- Secondary actions for one object (edit, duplicate, delete).
- Small view options: toggles (checkbox items) and one-of-many choices (radio items).

## When not to use

- Primary or single actions: use a [`Button`](https://docs.nasaqui.com/components/button).
- Navigating the whole app or searching for commands: use [`CommandPalette`](https://docs.nasaqui.com/components/command-palette).
- The signed-in user's account menu: use [`UserMenu`](https://docs.nasaqui.com/components/user-menu).
- Form selection: use a select or field component.

## Import

```tsx
import {
  DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuItem,
} from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import {
  Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem,
  DropdownMenuSeparator, DropdownMenuTrigger,
} from "@fadymondy/nasaq/web";
import { ChevronDown, Copy, Pencil, Trash2 } from "lucide-react";

export function IssueActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button />}>
        Actions <ChevronDown />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem shortcut="E">
          <Pencil /> Edit
        </DropdownMenuItem>
        <DropdownMenuItem shortcut="⌘D">
          <Copy /> Duplicate
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="danger">
          <Trash2 /> Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

## Anatomy

```
DropdownMenu                        Base UI Menu.Root
├─ DropdownMenuTrigger              Base UI Menu.Trigger (use render={<Button />})
└─ DropdownMenuContent              Portal + positioner + popup   data-slot="dropdown-menu-content"
   ├─ DropdownMenuGroup             Base UI Menu.Group
   │  ├─ DropdownMenuLabel          data-slot="dropdown-menu-label"
   │  └─ DropdownMenuItem           data-slot="dropdown-menu-item", data-variant="default|danger"
   │     └─ DropdownMenuShortcut    data-slot="dropdown-menu-shortcut" (from the shortcut prop)
   ├─ DropdownMenuSeparator         data-slot="dropdown-menu-separator"
   ├─ DropdownMenuCheckboxItem      data-slot="dropdown-menu-checkbox-item"
   ├─ DropdownMenuRadioGroup
   │  └─ DropdownMenuRadioItem      data-slot="dropdown-menu-radio-item"
   └─ DropdownMenuSub               Base UI Menu.SubmenuRoot
      ├─ DropdownMenuSubTrigger     data-slot="dropdown-menu-sub-trigger" (chevron added)
      └─ DropdownMenuSubContent     data-slot="dropdown-menu-sub-content"
```

## API

### Base UI aliases

`DropdownMenu` = `Menu.Root`, `DropdownMenuTrigger` = `Menu.Trigger`, `DropdownMenuGroup` = `Menu.Group`,
`DropdownMenuRadioGroup` = `Menu.RadioGroup`, `DropdownMenuSub` = `Menu.SubmenuRoot`. They take the
Base UI props.

| Export | Common props |
| --- | --- |
| `DropdownMenu` | `open?`, `defaultOpen?`, `onOpenChange?`, `modal?`, `loopFocus?`, `disabled?` |
| `DropdownMenuTrigger` | `render?` (e.g. `<Button />`), `disabled?` |
| `DropdownMenuRadioGroup` | `value?`, `defaultValue?`, `onValueChange?` |
| `DropdownMenuSub` | `open?`, `defaultOpen?`, `onOpenChange?` |

### `DropdownMenuContent`

`DropdownMenuContentProps` extends Base UI `Menu.Popup` props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side?` | `Menu.Positioner` `side` (`"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"`) | `"bottom"` | Which side of the trigger to open on. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | Alignment against the trigger. |
| `sideOffset?` | `number` | `4` | Gap to the trigger in px. |
| `className?` | `string` | none | Merged onto the popup. |

Popup: `min-w-44`, `max-h-[var(--available-height)]`, scrolls when needed.

### `DropdownMenuItem`

`DropdownMenuItemProps` extends Base UI `Menu.Item` props (`onClick`, `disabled`, `closeOnClick`, `label`, `render`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant?` | `"default" \| "danger"` | `"default"` | `danger` uses `text-nq-danger-text`. Sets `data-variant`. |
| `shortcut?` | `ReactNode` | none | Renders a `DropdownMenuShortcut` at the inline end. |
| `children?` | `ReactNode` | none | Icon and label. |

### `DropdownMenuCheckboxItem`

Base UI `Menu.CheckboxItem` props: `checked?`, `defaultChecked?`, `onCheckedChange?(checked: boolean)`,
`disabled?`, `closeOnClick?` (default `false`). A check icon shows at the inline-start.

### `DropdownMenuRadioItem`

Base UI `Menu.RadioItem` props: `value` (required), `disabled?`, `closeOnClick?`. A dot shows at the
inline-start when selected. Place inside `DropdownMenuRadioGroup`.

### `DropdownMenuLabel`

Inside a `DropdownMenuGroup` it is Base UI `Menu.GroupLabel` and labels the group (`aria-labelledby`). Outside a group it renders a plain presentational heading (`role="presentation"`) with the same style, so it no longer throws. Wrap a label and its items in `<DropdownMenuGroup>` when you want the group announced.

### `DropdownMenuSeparator`

Base UI `Menu.Separator` props. A 1px line that bleeds to the popup edges (`-mx-1.5`).

### `DropdownMenuShortcut`

`ComponentProps<"span">`. Rendered with `dir="ltr"`, monospace, at the end (`ms-auto`). Shortcut hints are
not mirrored.

### `DropdownMenuSubTrigger`

Base UI `Menu.SubmenuTrigger` props. Appends a directional chevron (`<Icon directional />`) that flips in RTL.

### `DropdownMenuSubContent`

`DropdownMenuSubContentProps` extends Base UI `Menu.Popup` props.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side?` | as `Menu.Positioner` `side` | `"inline-end"` | Preferred side of the sub-trigger. Logical, so it mirrors in RTL. |
| `align?` | as `Menu.Positioner` `align` | `"start"` | Alignment on the cross axis. |
| `sideOffset?` | `number` | `-4` | Gap to the trigger in px (negative overlaps slightly). |

## Examples

### Toggles, radio choices and a submenu

```tsx
import {
  Button, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent,
  DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSub,
  DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger,
} from "@fadymondy/nasaq/web";
import { useState } from "react";

export function ViewOptions() {
  const [archived, setArchived] = useState(false);
  const [sort, setSort] = useState("updated");
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button />}>View</DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuCheckboxItem checked={archived} onCheckedChange={setArchived}>
          Show archived
        </DropdownMenuCheckboxItem>
        <DropdownMenuSub>
          <DropdownMenuSubTrigger>Sort by</DropdownMenuSubTrigger>
          <DropdownMenuSubContent>
            <DropdownMenuRadioGroup value={sort} onValueChange={setSort}>
              <DropdownMenuRadioItem value="updated">Last updated</DropdownMenuRadioItem>
              <DropdownMenuRadioItem value="priority">Priority</DropdownMenuRadioItem>
              <DropdownMenuRadioItem value="title">Title</DropdownMenuRadioItem>
            </DropdownMenuRadioGroup>
          </DropdownMenuSubContent>
        </DropdownMenuSub>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

### Labelled group, Arabic, aligned to the end

```tsx
import {
  Button, DropdownMenu, DropdownMenuContent, DropdownMenuGroup,
  DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger,
} from "@fadymondy/nasaq/web";
import { Pencil, Trash2 } from "lucide-react";

export function RowActionsAr() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button variant="ghost" />}>إجراءات</DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuGroup>
          <DropdownMenuLabel>المهمة</DropdownMenuLabel>
          <DropdownMenuItem>
            <Pencil /> تعديل
          </DropdownMenuItem>
          <DropdownMenuItem variant="danger">
            <Trash2 /> حذف
          </DropdownMenuItem>
        </DropdownMenuGroup>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

## Accessibility

Provided by Base UI Menu: trigger has `aria-haspopup="menu"` and `aria-expanded`; the popup has
`role="menu"`; items have `role="menuitem"`, `"menuitemcheckbox"` or `"menuitemradio"` with
`aria-checked`. Focus moves into the menu on open and returns to the trigger on close.

| Key | Action |
| --- | --- |
| `Enter` / `Space` / `↓` on trigger | Opens the menu and focuses the first item. |
| `↑` / `↓` | Moves the highlight between items. |
| `Home` / `End` | First / last item. |
| Typing characters | Typeahead to the matching item. |
| `Enter` / `Space` | Activates the highlighted item (closes the menu, except checkbox items). |
| `→` (LTR) / `←` (RTL) on a sub-trigger | Opens the submenu. |
| `←` (LTR) / `→` (RTL) inside a submenu | Closes the submenu. |
| `Esc` | Closes the menu. |

- Give an icon-only trigger an `aria-label`.
- Item text and labels are the caller's; localise them.
- Disabled items are skipped by `data-disabled` styling and Base UI.

## RTL & i18n

- Check/radio indicators use `start-2.5` / `ps-8`; the shortcut and sub-trigger chevron use `ms-auto`. All flip with `dir`.
- The submenu opens at `inline-end`, so it opens to the left in RTL.
- The sub-trigger chevron is `directional` and mirrors.
- Shortcut text is forced `dir="ltr"`.
- No built-in strings.

## Styling & tokens

- Surface level 3: `bg-popover`, `border-border`, `rounded-floating`, `shadow-floating`. Items `rounded-control`, `h-nav-row`, `text-body-sm`.
- Highlighted item: `data-highlighted:bg-nq-selected`. Open sub-trigger: `data-popup-open:bg-nq-selected`. Disabled: `data-disabled`.
- Danger items: `text-nq-danger-text`.
- Target `[data-slot=dropdown-menu-content]`, `[data-slot=dropdown-menu-item][data-variant=danger]`.

## Do / Don't

- **Do** put destructive items last, after a separator, with `variant="danger"` and a destructive icon.
- **Do** confirm irreversible actions in a [`Dialog`](https://docs.nasaqui.com/components/dialog) rather than acting on click.
- **Don't** hide the only path to an important action inside a menu.
- **Don't** nest submenus more than one level deep.
- **Don't** use `left` / `right` for `side` when `inline-start` / `inline-end` is meant.

## Related

- [Button](https://docs.nasaqui.com/components/button) · [UserMenu](https://docs.nasaqui.com/components/user-menu) · [Dialog](https://docs.nasaqui.com/components/dialog)
- [CommandPalette](https://docs.nasaqui.com/components/command-palette)

## Lab

https://docs.nasaqui.com/?path=/docs/components-overlays-dropdown-menu--docs

## Code

### React

```tsx
import {
  Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem,
  DropdownMenuSeparator, DropdownMenuTrigger,
} from "@fadymondy/nasaq/web";
import { ChevronDown, Copy, Pencil, Trash2 } from "lucide-react";

export function IssueActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button />}>
        Actions <ChevronDown />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem shortcut="E">
          <Pencil /> Edit
        </DropdownMenuItem>
        <DropdownMenuItem shortcut="⌘D">
          <Copy /> Duplicate
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="danger">
          <Trash2 /> Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

### shadcn

```tsx
import { Button } from "@/components/ui/button";
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { ChevronDown, Copy, Pencil, Trash2 } from "lucide-react";

export function IssueActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger render={<Button />}>
        Actions <ChevronDown />
      </DropdownMenuTrigger>
      <DropdownMenuContent>
        <DropdownMenuItem shortcut="E">
          <Pencil /> Edit
        </DropdownMenuItem>
        <DropdownMenuItem shortcut="⌘D">
          <Copy /> Duplicate
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="danger">
          <Trash2 /> Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { ChevronDown, Copy, Pencil, Trash2 } from "lucide-vue-next";
import { NqButton, NqDropdownMenu, NqDropdownMenuContent, NqDropdownMenuItem, NqDropdownMenuSeparator, NqDropdownMenuTrigger } from "@fadymondy/nasaq/vue";
</script>

<template>
  <NqDropdownMenu>
    <NqDropdownMenuTrigger as-child>
      <NqButton>Actions <ChevronDown /></NqButton>
    </NqDropdownMenuTrigger>
    <NqDropdownMenuContent>
      <NqDropdownMenuItem shortcut="E"><Pencil /> Edit</NqDropdownMenuItem>
      <NqDropdownMenuItem shortcut="⌘D"><Copy /> Duplicate</NqDropdownMenuItem>
      <NqDropdownMenuSeparator />
      <NqDropdownMenuItem variant="danger"><Trash2 /> Delete</NqDropdownMenuItem>
    </NqDropdownMenuContent>
  </NqDropdownMenu>
</template>
```

### Blade

```blade
<x-nq::dropdown-menu>
    <x-nq::dropdown-menu.trigger>Actions <x-lucide-chevron-down /></x-nq::dropdown-menu.trigger>
    <x-nq::dropdown-menu.content>
        <x-nq::dropdown-menu.item shortcut="E"><x-lucide-pencil /> Edit</x-nq::dropdown-menu.item>
        <x-nq::dropdown-menu.item shortcut="⌘D"><x-lucide-copy /> Duplicate</x-nq::dropdown-menu.item>
        <x-nq::dropdown-menu.separator />
        <x-nq::dropdown-menu.item variant="danger"><x-lucide-trash-2 /> Delete</x-nq::dropdown-menu.item>
    </x-nq::dropdown-menu.content>
</x-nq::dropdown-menu>
```

### HTML + Alpine

```html
<div data-slot="dropdown-menu" x-data="nqDropdownMenu(false)" x-modelable="open" class="contents"><button data-slot="dropdown-menu-trigger"
     type="button"                         x-bind="trigger" x-ref="trigger" class="inline-flex shrink-0 select-none items-center justify-center gap-2 whitespace-nowrap rounded-control border font-sans text-label transition-colors duration-150 ease-nq min-h-[var(--nq-touch-min,0px)] outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-nq-focus disabled:pointer-events-none disabled:opacity-50 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0 border-border bg-card text-foreground hover:bg-nq-hover h-control px-[var(--nq-control-pad)]">
        Actions <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m6 9 6 6 6-6"/>
</svg></button>
    <template x-teleport="body">
    <div data-slot="dropdown-menu-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-44 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0"><div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/>
  <path d="m15 5 4 4"/>
</svg> Edit<span data-slot="dropdown-menu-shortcut" dir="ltr" class="ms-auto font-mono text-[11px] text-muted-foreground">E</span>
</div>
        <div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <rect width="14" height="14" x="8" y="8" rx="2" ry="2"/>
  <path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/>
</svg> Duplicate<span data-slot="dropdown-menu-shortcut" dir="ltr" class="ms-auto font-mono text-[11px] text-muted-foreground">⌘D</span>
</div>
        <div data-slot="dropdown-menu-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
        <div data-slot="dropdown-menu-item" role="menuitem" data-variant="danger" x-bind="item"
        class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 text-nq-danger-text [&_svg]:text-current"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M10 11v6"/>
  <path d="M14 11v6"/>
  <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6"/>
  <path d="M3 6h18"/>
  <path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
</svg> Delete</div></div>
</template></div>
```
