Skip to main content

Dropdown Menu

A button-triggered menu with items, checkboxes, radio groups, and submenus.

import * as DropdownMenu from '@gentleduck/primitives/dropdown-menu'
import * as DropdownMenu from '@gentleduck/primitives/dropdown-menu'

Anatomy

<DropdownMenu.Root>
  <DropdownMenu.Trigger />
  <DropdownMenu.Portal>
    <DropdownMenu.Content>
      <DropdownMenu.Label />
      <DropdownMenu.Item />
      <DropdownMenu.Group>
        <DropdownMenu.Item />
      </DropdownMenu.Group>
      <DropdownMenu.CheckboxItem>
        <DropdownMenu.ItemIndicator />
      </DropdownMenu.CheckboxItem>
      <DropdownMenu.RadioGroup>
        <DropdownMenu.RadioItem>
          <DropdownMenu.ItemIndicator />
        </DropdownMenu.RadioItem>
      </DropdownMenu.RadioGroup>
      <DropdownMenu.Separator />
      <DropdownMenu.Sub>
        <DropdownMenu.SubTrigger />
        <DropdownMenu.SubContent />
      </DropdownMenu.Sub>
      <DropdownMenu.Arrow />
    </DropdownMenu.Content>
  </DropdownMenu.Portal>
</DropdownMenu.Root>
<DropdownMenu.Root>
  <DropdownMenu.Trigger />
  <DropdownMenu.Portal>
    <DropdownMenu.Content>
      <DropdownMenu.Label />
      <DropdownMenu.Item />
      <DropdownMenu.Group>
        <DropdownMenu.Item />
      </DropdownMenu.Group>
      <DropdownMenu.CheckboxItem>
        <DropdownMenu.ItemIndicator />
      </DropdownMenu.CheckboxItem>
      <DropdownMenu.RadioGroup>
        <DropdownMenu.RadioItem>
          <DropdownMenu.ItemIndicator />
        </DropdownMenu.RadioItem>
      </DropdownMenu.RadioGroup>
      <DropdownMenu.Separator />
      <DropdownMenu.Sub>
        <DropdownMenu.SubTrigger />
        <DropdownMenu.SubContent />
      </DropdownMenu.Sub>
      <DropdownMenu.Arrow />
    </DropdownMenu.Content>
  </DropdownMenu.Portal>
</DropdownMenu.Root>

Example

import * as DropdownMenu from '@gentleduck/primitives/dropdown-menu'
 
function ActionsMenu() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger className="px-3 py-2 border rounded">
        Actions
      </DropdownMenu.Trigger>
 
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="bg-white shadow-lg rounded-md p-1 min-w-[180px] border" sideOffset={4}>
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
            Edit
          </DropdownMenu.Item>
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
            Duplicate
          </DropdownMenu.Item>
          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
          <DropdownMenu.Sub>
            <DropdownMenu.SubTrigger className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer flex justify-between">
              Share <span>></span>
            </DropdownMenu.SubTrigger>
            <DropdownMenu.SubContent className="bg-white shadow-lg rounded-md p-1 min-w-[140px] border">
              <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
                Copy Link
              </DropdownMenu.Item>
              <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
                Email
              </DropdownMenu.Item>
            </DropdownMenu.SubContent>
          </DropdownMenu.Sub>
          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer text-red-600">
            Delete
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}
import * as DropdownMenu from '@gentleduck/primitives/dropdown-menu'
 
function ActionsMenu() {
  return (
    <DropdownMenu.Root>
      <DropdownMenu.Trigger className="px-3 py-2 border rounded">
        Actions
      </DropdownMenu.Trigger>
 
      <DropdownMenu.Portal>
        <DropdownMenu.Content className="bg-white shadow-lg rounded-md p-1 min-w-[180px] border" sideOffset={4}>
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
            Edit
          </DropdownMenu.Item>
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
            Duplicate
          </DropdownMenu.Item>
          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
          <DropdownMenu.Sub>
            <DropdownMenu.SubTrigger className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer flex justify-between">
              Share <span>></span>
            </DropdownMenu.SubTrigger>
            <DropdownMenu.SubContent className="bg-white shadow-lg rounded-md p-1 min-w-[140px] border">
              <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
                Copy Link
              </DropdownMenu.Item>
              <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer">
                Email
              </DropdownMenu.Item>
            </DropdownMenu.SubContent>
          </DropdownMenu.Sub>
          <DropdownMenu.Separator className="h-px bg-gray-200 my-1" />
          <DropdownMenu.Item className="px-3 py-1.5 rounded hover:bg-gray-100 cursor-pointer text-red-600">
            Delete
          </DropdownMenu.Item>
        </DropdownMenu.Content>
      </DropdownMenu.Portal>
    </DropdownMenu.Root>
  )
}

API

The root component that manages open/closed state and provides context. Wraps the base Menu primitive internally.

PropTypeDefaultDescription
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state (uncontrolled)
onOpenChange(open: boolean) => void-Called when the open state should change
dir'ltr' | 'rtl'-Text direction. Resolved with useDirection (dir prop -> DirectionProvider -> 'ltr').
modalbooleantrueWhen true, interaction with outside elements is disabled and only menu content is visible to screen readers

Button that toggles the dropdown menu. Renders a <button> with aria-haspopup="menu".

PropTypeDefaultDescription
asChildboolean-Render as the child element instead of a <button>
disabledbooleanfalseDisables the trigger

Sets aria-expanded, aria-controls, and data-state automatically.

Renders the dropdown content into a portal.

PropTypeDefaultDescription
containerElement | nulldocument.bodyPortal target

The dropdown content. Handles focus management, keyboard navigation, and dismiss behavior.

PropTypeDefaultDescription
side'top' | 'right' | 'bottom' | 'left''bottom'Preferred side relative to the trigger
sideOffsetnumber-Main-axis offset from trigger
align'start' | 'center' | 'end'-Cross-axis alignment
alignOffsetnumber-Cross-axis offset
avoidCollisionsbooleantrueFlip to avoid viewport overflow
collisionPaddingnumber-Padding from viewport edges
onCloseAutoFocus(event: Event) => void-Called when focus returns to trigger on close
onEscapeKeyDown(event: KeyboardEvent) => void-Called when Escape is pressed
onPointerDownOutside(event) => void-Called when clicking outside
onInteractOutside(event) => void-Called on any interaction outside
trapFocusbooleancontext.openOverride whether focus is trapped inside the content
disableOutsidePointerEventsbooleancontext.openOverride whether pointer events outside are blocked
disableOutsideScrollbooleantrueOverride whether body scroll is locked while open

Exposes data-state="open" / data-state="closed" and data-side for CSS animation.

CSS custom properties available:

  • --gentleduck-dropdown-menu-content-transform-origin
  • --gentleduck-dropdown-menu-content-available-width
  • --gentleduck-dropdown-menu-content-available-height
  • --gentleduck-dropdown-menu-trigger-width
  • --gentleduck-dropdown-menu-trigger-height

Groups related items. Renders a <div> with role="group".

Non-interactive label for a group.

An interactive menu item. Fires onSelect when activated.

PropTypeDefaultDescription
disabledbooleanfalseDisable the item
onSelect(event: Event) => void-Called when item is selected
textValuestring-Text override for typeahead search

Exposes data-highlighted when focused and data-disabled when disabled.

A toggleable menu item with checked state.

PropTypeDefaultDescription
checkedboolean | 'indeterminate'-Checked state
onCheckedChange(checked: boolean) => void-Called on toggle
disabledbooleanfalseDisable the item

Mutually exclusive menu items.

Prop (RadioGroup)TypeDefaultDescription
valuestring-Currently selected value
onValueChange(value: string) => void-Called when selection changes
Prop (RadioItem)TypeDefaultDescription
valuestring-Value for this item
disabledbooleanfalseDisable the item

Renders only when the parent item is checked. Use for check marks or radio dots.

PropTypeDefaultDescription
forceMountboolean-Keep mounted for animation control

Nested submenus.

Prop (Sub)TypeDefaultDescription
openboolean-Controlled open state
defaultOpenbooleanfalseInitial open state
onOpenChange(open: boolean) => void-Called when open state changes

SubContent exposes the same --gentleduck-dropdown-menu-* CSS custom properties as Content.

Visual separator between groups. Renders a <div> with aria-hidden.

Arrow pointing to the trigger.

PropTypeDefaultDescription
widthnumber10Arrow width in pixels
heightnumber5Arrow height in pixels

Keyboard interactions

KeyAction
Space / EnterOpens menu (on trigger) or activates highlighted item
ArrowDownOpens menu or highlights next item
ArrowUpHighlights previous item
ArrowRightOpens submenu (on SubTrigger)
ArrowLeftCloses submenu
EscapeCloses menu