# DropdownMenu

A menu of actions.

- Group: Overlays
- Import: `import { DropdownMenu } from '@misoto22/folio'`
- Page: https://ui.misoto22.com/components/dropdown-menu/
- Related: dialog, select

## When to reach for it

Actions. Items that navigate belong in a nav; items that set a value are a Select or a RadioGroup.

## Anatomy

- **Trigger** (required) — A passthrough to Radix, which renders its own bare button unless you pass asChild. It carries aria-haspopup and data-state, which is what lets a Button show that its menu is open.
- **Panel** (required) — The portalled menu: at least 11rem wide, 6px off the trigger, colliding with 8px of padding against the viewport or the OverlayContainer frame. It has no max height, so it flips rather than scrolls.
- **Item** — A row. icon takes either spelling — the icon component, sized to 16px here, or a rendered element, placed as given; destructive paints the row --danger; disabled drops pointer events and the opacity.
- **Label** — A mono eyebrow, on its own. Visual only — Radix renders it as a plain div, and the arrow keys skip it. DropdownMenuGroup is the one that heads rows: it renders role="group" and points its aria-labelledby at this.
- **Group** — A named section: role="group" around the rows, with label rendered inside it as the Label and named through aria-labelledby. The wiring is the component’s job because a caller doing it by hand has to invent an id.
- **Separator** — A hairline between groups, and a real role="separator" — which a menu permits, unlike the listbox a Command palette is built on.

## Best practices

### Do

- Pass icon the component — icon={Settings} — and let the row size it, so a menu of ten rows draws ten icons at one size rather than ten sizes. The element spelling is accepted too, and used to be the exact opposite of what CommandItem took one import away.
- Give the trigger asChild and a real Button: without it Radix renders its own unstyled button, and the menu ends up hanging off a control that is not part of the system’s set.
- Hold a dialog’s open state yourself and call event.preventDefault() in the item’s onSelect — selecting a row closes the menu, and Radix’s close moves focus back to the trigger, which arrives after the dialog has claimed it and pulls the reader straight back out.
- Stop at about a dozen rows. There is no max height on the panel, so a longer menu grows until it hits the collision padding and flips above the trigger — SearchableMenu is the same list once it has outgrown this one.

### Don’t

- Radix defaults modal to true and nothing here overrides it, so while the menu is open the page behind is scroll-locked and its pointer events are off — a menu is not the place for something the reader is meant to consult the page while using.
- A bare DropdownMenuLabel over rows is a picture of a heading: Radix’s MenuLabel is a plain div with no role and nothing tying it to what follows, so the sections a sighted reader sees arrive as one undivided list. DropdownMenuGroup renders both halves and wires them together.
- There is no checkbox item, radio item or submenu in this package’s exports — a menu that needs a checked state has to import from @radix-ui/react-dropdown-menu directly, and that row arrives with none of this file’s styling on it.

## Accessibility

- Inside a bounded frame — a device preview, an embedded console — wrap the subtree in `<OverlayContainer container={el}>`. The panel then renders into that element and collides with its edges instead of the viewport’s, and inherits the `dir` and `data-density` set there.
- Highlight is driven by data-highlighted, which covers hover AND keyboard focus — styling :hover alone leaves the keyboard user unable to see where they are.
- DropdownMenuGroup segments the menu for a screen reader as well as for the eye; a bare Label does it for the eye alone.

## Keyboard

- Enter / Space / ↓ — Opens the menu and lands on the first item.
- ↑ / ↓ — Moves between items.
- a–z — Jumps to the next item starting with that letter.
- Escape — Closes the menu and returns focus to the trigger.

## DropdownMenu

Re-export of `DropdownMenuPrimitive.Root`.

Radix DropdownMenu root + trigger, re-exported as typed passthroughs.

## DropdownMenuTrigger

Re-export of `DropdownMenuPrimitive.Trigger`.

## DropdownMenuContent

A menu surface. A menu is a list of ACTIONS. If the items navigate somewhere, they belong in a nav; if they set a value, that is a `Select` or a `RadioGroup` — this package ships no checkbox or radio menu item, and a plain item pretending to be a choice loses the checked state a screen reader needs.

Also accepts: `ComponentProps<typeof DropdownMenuPrimitive.Content>`.

## DropdownMenuItem

A menu row. Highlight is driven by Radix's `data-highlighted`, which covers both pointer hover and keyboard focus — styling `:hover` alone leaves the keyboard user unable to see where they are.

### Props

- `icon` — `RemixiconComponentType | ReactNode`. Optional leading icon, rendered before the label. Either spelling: `icon={Settings}` passes the component and this sizes it, `icon={<Settings size={16} />}` passes the element and this places it. The two used to mean opposite things one import apart — `CommandItem.icon` took the element while this took the component — and the wrong one did not fail a type check into anything actionable, it failed at render.
- `destructive` — `boolean` default `false`. Paints the row as destructive. Use for delete, revoke, disconnect.

Also accepts: `ComponentProps<typeof DropdownMenuPrimitive.Item>`.

## DropdownMenuSeparator

Hairline divider between menu groups.

## DropdownMenuLabel

Mono eyebrow heading, on its own. Visual only: Radix renders it as a bare `<div>` with no role, so it segments the menu for a reader who can see it and for nobody else. Reach for `DropdownMenuGroup` when the eyebrow is a HEADING over rows; this is right for a line that heads nothing — the signed-in address at the top of an account menu.

## DropdownMenuGroup

A named section of a menu. The eyebrow alone was a picture of a heading. Radix's `MenuLabel` carries no role and no `aria-labelledby` wiring, and `MenuGroup` — which does carry `role="group"` — was not re-exported by this package at all, so a sighted reader saw three labelled sections and a screen-reader user got one undifferentiated list. This renders the group, renders the label inside it, and points the one at the other, which is the whole of the fix and not something a caller should have to remember.

### Props

- `label` — `ReactNode`. The eyebrow over the rows, and the group's accessible name.

Also accepts: `ComponentProps<typeof DropdownMenuPrimitive.Group>`.

## Example — an account menu

```tsx
<DropdownMenu>
  <DropdownMenuTrigger asChild>
    <Button variant="secondary">Account</Button>
  </DropdownMenuTrigger>
  <DropdownMenuContent align="start">
    <DropdownMenuLabel>Signed in</DropdownMenuLabel>
    <DropdownMenuItem icon={RiSettings3Line}>Settings</DropdownMenuItem>
    <DropdownMenuItem icon={RiLogoutBoxRLine}>Sign out</DropdownMenuItem>
    <DropdownMenuSeparator />
    <DropdownMenuItem icon={RiDeleteBinLine} destructive>Delete account</DropdownMenuItem>
  </DropdownMenuContent>
</DropdownMenu>
```

## Example — an overflow button

```tsx
<div className="flex w-full max-w-sm items-center justify-between gap-3 rounded-(--radius) border border-(--rule) px-4 py-3">
  <Text size="sm" tone="strong" className="font-mono">
    kyoto-february.tif
  </Text>
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button iconOnly aria-label="Actions for kyoto-february.tif" variant="ghost" size="sm">
        <RiMoreLine size={16} aria-hidden />
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
      <DropdownMenuItem icon={RiFileCopyLine}>Copy link</DropdownMenuItem>
      <DropdownMenuItem icon={RiDownloadLine}>Download original</DropdownMenuItem>
      <DropdownMenuItem icon={RiPushpinLine} disabled>
        Pin to the collection
      </DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuItem icon={RiDeleteBinLine} destructive>
        Delete
      </DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</div>
```

## Example — where it opens

```tsx
<div className="flex w-full max-w-md items-center justify-between gap-3 rounded-(--radius) border border-(--rule) p-2">
  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="ghost" size="sm">
        <RiFilterLine size={16} aria-hidden /> Filter
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="start">
      <DropdownMenuItem icon={RiLayoutRowLine}>Failed runs only</DropdownMenuItem>
      <DropdownMenuItem icon={RiTableLine}>Main branch only</DropdownMenuItem>
      <DropdownMenuItem icon={RiEqualizerLine}>Longer than two minutes</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>

  <DropdownMenu>
    <DropdownMenuTrigger asChild>
      <Button variant="ghost" size="sm">
        <RiArrowUpDownLine size={16} aria-hidden /> Sort
      </Button>
    </DropdownMenuTrigger>
    <DropdownMenuContent align="end">
      <DropdownMenuItem>Newest first</DropdownMenuItem>
      <DropdownMenuItem>Longest first</DropdownMenuItem>
      <DropdownMenuItem>Branch, A to Z</DropdownMenuItem>
    </DropdownMenuContent>
  </DropdownMenu>
</div>
```
