Starter Docs
UI

Dropdown Menu

WAI-ARIA's menu button on the native Popover API, with icons, shortcuts, checkbox and radio rows, submenus, typeahead and a highlight that glides between rows. Plus a right-click ContextMenu.

Installation

Add the registry to components.json, once

components.json
{
  "registries": {
    "@rabtx": "http://localhost:3002/r/{name}.json"
  }
}

Add the component

npx shadcn@latest add @rabtx/dropdown-menu

Or, without the namespace, by URL:

npx shadcn@latest add http://localhost:3002/r/dropdown-menu.json

The files land in your components/ui and are styled by your own theme. Add --overwrite to replace a shadcn component with the same name.

The menu is a popover: installing it also installs popover for its placement and motion.

Usage

Installed from the registry, import from @/components/ui/dropdown-menu; inside this monorepo, from @rabtx/ui/dropdown-menu.

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuSeparator,
  DropdownMenuTrigger,
} from "@rabtx/ui/dropdown-menu";

export function RowActions() {
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="ghost" size="icon" aria-label="Actions">
          <HugeiconsIcon icon={MoreHorizontalIcon} />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem icon={<HugeiconsIcon icon={Copy01Icon} />} shortcut="⌘D">
          Duplicate
        </DropdownMenuItem>
        <DropdownMenuSeparator />
        <DropdownMenuItem variant="destructive" onSelect={remove}>
          Delete
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

The parts and data-slot names are shadcn's. The content is a role="menu" [popover] and the trigger its native invoker, so the top layer, a press outside, Escape and focus going back to the trigger come from the browser. The component adds the menu keyboard: roving focus over the rows, arrows, Home and End, typeahead and submenus.

Icons, shortcuts and a destructive action

A row takes an icon, a shortcut hint, variant="destructive" and disabled. Disabled rows are skipped by the keys and cannot be chosen.

Q3 roadmapLast action: Nothing yet

Checkboxes

DropdownMenuCheckboxItem is a menuitemcheckbox; its tick draws itself in on the check spring. Choosing a row closes the menu; call event.preventDefault() in onSelect to keep it open while toggling several.

Radio group

DropdownMenuRadioGroup holds the value; each DropdownMenuRadioItem is a menuitemradio whose dot pops in on the swap spring. A DropdownMenuLabel inside a group (or radio group) names it.

Context menu

ContextMenu opens the same menu at the pointer on a right-click over ContextMenuTrigger, or under the focused element with the menu key or Shift+F10 (landing on the first row). It is anchored to an invisible point where the press was, unfolds from it, and flips near the edges. The rows are the dropdown menu's, exported again as ContextMenuItem, ContextMenuCheckboxItem and so on.

Right-click the card, or focus it and press Shift+F10

API

DropdownMenu (and ContextMenu) hold the state:

Prop

Type

DropdownMenuContent (and DropdownMenuSubContent, ContextMenuContent):

Prop

Type

DropdownMenuItem:

Prop

Type

DropdownMenuCheckboxItem adds checked, defaultChecked and onCheckedChange; DropdownMenuRadioGroup takes value, defaultValue and onValueChange, and each DropdownMenuRadioItem a value. DropdownMenuSub wraps a DropdownMenuSubTrigger and its DropdownMenuSubContent; one submenu per menu is open at a time. DropdownMenuLabel, DropdownMenuGroup, DropdownMenuSeparator and DropdownMenuShortcut are divs and a span.

Theming

PartTokens
Panelthe popover's: --popover, --popover-foreground, the surface edge tokens, --radius-box
Rowstext --popover-foreground, icons, labels and shortcuts --muted-foreground, corners --radius-item
Highlight--accent (text --accent-foreground); destructive rows --destructive and a 12% tint of it
Separator--border
Motion--spring-unfold (panel), --spring-check (tick), --spring-swap (dot), --motion-ease

How it works

EffectCSS
Placement, flip, unfoldthe popover's: anchor positioning, try fallbacks and a reveal from the anchor side
Highlightthe active row (focused, or a submenu's open row) takes anchor-name; one highlight, the panel's last child, is placed on it with anchor() and its top and bottom glide in 160ms. anchor-scope keeps each menu's rows to itself
Tickstroke-dashoffset from 1 to 0 over pathLength="1" on the check spring
Dotscale from 0 on the swap spring
Submenua nested [popover] anchored to its row, side="right", flipping left near the edge

Without anchor scoping, the active row fills itself instead of the gliding highlight.

Accessibility

  • The trigger has aria-haspopup="menu", aria-expanded and aria-controls, and names the menu (aria-labelledby).
  • Rows are menuitem, menuitemcheckbox (with aria-checked) and menuitemradio; disabled rows have aria-disabled; groups are role="group" named by their label; separators are role="separator".
  • Enter, Space and ArrowDown open the menu on the first row, ArrowUp on the last; a click opens it with focus on the menu. Arrows move and wrap, Home and End jump, letters jump by typeahead, Enter and Space choose. ArrowRight, Enter or Space open a submenu on its first row, ArrowLeft and Escape close it and go back to its row. Escape closes the menu and Tab closes it and moves on; focus goes back to the trigger.

On this page