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".

PropTypeDescription
asChildbooleanRender as the child element instead of a <button>
disabledbooleanDisables 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

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.

PropTypeDescription
disabledbooleanDisable the item
onSelect(event: Event) => voidCalled when item is selected
textValuestringText override for typeahead search

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

A toggleable menu item with checked state.

PropTypeDescription
checkedboolean | 'indeterminate'Checked state
onCheckedChange(checked: boolean) => voidCalled on toggle
disabledbooleanDisable the item

Mutually exclusive menu items.

Prop (RadioGroup)TypeDescription
valuestringCurrently selected value
onValueChange(value: string) => voidCalled when selection changes
Prop (RadioItem)TypeDescription
valuestringValue for this item
disabledbooleanDisable the item

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

PropTypeDescription
forceMountbooleanKeep 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