---
title: DropdownMenu
description: Triggered action menu anchored to its child content.
sidebar:
  order: 6
search:
  tags: [actions, menu, submenu, dropdown]
---

# DropdownMenu

> Triggered action menu anchored to its child content.

## Import

```tsx
import { DropdownMenu } from 'moraine'
```

## Slot Structure

Wrapper trigger with a floating menu portal containing grouped items.

Use `contentProps` and `itemProps` to forward native attributes, refs, and event handlers. Item handlers run before built-in menu behavior and can call `preventDefault()` to cancel activation.

### Menu

```text
trigger
└── content (portal)
    └── group (×n)
        ├── label (optional)
        ├── separator (optional)
        └── item (×n)
```

### Item internals

```text
item
├── itemLeading (optional)
├── itemWrapper
│   ├── itemLabel (optional)
│   └── itemDescription (optional)
└── itemTrailing
    └── itemIndicator (optional, checkbox items)
```

## Examples

### Sizes

Menu item size scale from `sm` to `lg` for compact and roomy density.

```tsx
function Sizes() {
  const SIZES = ['sm', 'md', 'lg'] as const

  const ITEMS: DropdownMenuT.Item[] = [
    {
      type: 'group',
      children: [
        {
          label: 'Profile',
          icon: 'i-lucide-user',
        },
        {
          label: 'Settings',
          icon: 'i-lucide-settings-2',
        },
        {
          label: 'Sign Out',
          icon: 'i-lucide-log-out',
          color: 'destructive',
        },
      ],
    },
  ]

  return (
    <div class="flex flex-wrap gap-3">
      <For each={SIZES}>
        {(size) => (
          <DropdownMenu size={size} items={ITEMS}>
            <Button variant="outline" size="sm">
              {size} Menu
            </Button>
          </DropdownMenu>
        )}
      </For>
    </div>
  )
}
```

### Account / Team

An account dropdown with grouped actions, workspace switching, shortcut hints, and a destructive sign-out row.

```tsx
function AccountTeam() {
  const badgeClass =
    'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'

  const avatarClass =
    'grid size-4 place-items-center rounded-full bg-linear-to-br from-primary to-accent text-[10px] font-semibold text-primary-foreground'

  const [lastAction, setLastAction] = createSignal('None')

  return (
    <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
      <DropdownMenu
        items={[
          {
            type: 'group',
            label: 'Account',
            children: [
              {
                label: (
                  <div class="flex gap-2 items-center">
                    <span class="font-medium">Alex Morgan</span>
                    <span class={badgeClass}>Owner</span>
                  </div>
                ),
                description: 'alex@moraine.dev',
                icon: <span class={avatarClass}>AM</span>,
                onSelect: () => setLastAction('Open account profile'),
              },
              { type: 'separator' },
              {
                label: 'Switch Workspace',
                icon: 'i-lucide-building-2',
                children: [
                  {
                    type: 'group',
                    label: 'Recent Workspaces',
                    children: [
                      {
                        label: 'Design System',
                        description: '12 teammates · shared tokens',
                        icon: 'i-lucide-palette',
                        onSelect: () => setLastAction('Switch to Design System'),
                      },
                      {
                        label: 'Platform Ops',
                        description: '8 teammates · deploy tooling',
                        icon: 'i-lucide-server',
                        onSelect: () => setLastAction('Switch to Platform Ops'),
                      },
                      {
                        label: 'Support Workspace',
                        description: '5 teammates · customer issues',
                        icon: 'i-lucide-life-buoy',
                        onSelect: () => setLastAction('Switch to Support Workspace'),
                      },
                    ],
                  },
                  {
                    type: 'group',
                    label: 'Actions',
                    children: [
                      {
                        label: 'Create Workspace',
                        icon: 'i-lucide-plus',
                        onSelect: () => setLastAction('Create workspace'),
                      },
                    ],
                  },
                ],
              },
              {
                label: 'Invite Teammates',
                icon: 'i-lucide-user-plus',
                kbds: ['⌘', 'I'],
                onSelect: () => setLastAction('Invite teammates'),
              },
              {
                label: 'Billing & Usage',
                icon: 'i-lucide-credit-card',
                onSelect: () => setLastAction('Billing & usage'),
              },
            ],
          },
          {
            type: 'group',
            label: 'Preferences',
            children: [
              {
                label: 'Account Settings',
                icon: 'i-lucide-settings-2',
                kbds: ['⌘', ','],
                onSelect: () => setLastAction('Account settings'),
              },
              {
                label: 'Keyboard Shortcuts',
                icon: 'i-lucide-command',
                kbds: ['⌘', 'K'],
                onSelect: () => setLastAction('Keyboard shortcuts'),
              },
              {
                label: 'Support Inbox',
                icon: 'i-lucide-life-buoy',
                onSelect: () => setLastAction('Support inbox'),
              },
            ],
          },
          {
            type: 'group',
            children: [
              {
                label: 'Sign Out',
                icon: 'i-lucide-log-out',
                color: 'destructive',
                onSelect: () => setLastAction('Sign out'),
              },
            ],
          },
        ]}
      >
        <Button variant="outline">Open account menu</Button>
      </DropdownMenu>
      <p class="text-sm text-muted-foreground">
        Last action: <span class="font-medium">{lastAction()}</span>
      </p>
    </div>
  )
}
```

### Editor / View Options

A workspace-style menu with recent files, nested submenus, checkbox toggles, and theme selection for keyboard and pointer testing.

```tsx
function EditorViewOptions() {
  const [showLineNumbers, setShowLineNumbers] = createSignal(true)
  const [showMinimap, setShowMinimap] = createSignal(true)
  const [previewTabs, setPreviewTabs] = createSignal(false)
  const [autoSave, setAutoSave] = createSignal(true)
  const [theme, setTheme] = createSignal<'light' | 'dark' | 'system'>('dark')

  const editorItems = createMemo<DropdownMenuT.Item[]>(() => [
    {
      type: 'group',
      label: 'Editor',
      children: [
        {
          label: 'Command Palette',
          description: 'Jump to commands, files, and symbols',
          icon: 'i-lucide-search',
          kbds: ['⌘', 'K'],
        },
        {
          label: 'Go to File…',
          icon: 'i-lucide-file-search',
          kbds: ['⌘', 'P'],
        },
        {
          label: 'Open Recent',
          icon: 'i-lucide-history',
          children: [
            {
              type: 'group',
              label: 'Recent Files',
              children: [
                {
                  label: 'src/overlays/dropdown-menu/dropdown-menu.tsx',
                  icon: 'i-lucide-file-code-2',
                },
                {
                  label: 'docs/components/context-menu-demos.tsx',
                  icon: 'i-lucide-file-code-2',
                },
                {
                  label: 'Pinned Workspaces',
                  icon: 'i-lucide-star',
                  children: [
                    {
                      type: 'group',
                      children: [
                        {
                          label: 'Moraine',
                          description: 'packages + docs',
                          icon: 'i-lucide-folder-kanban',
                        },
                        {
                          label: 'Docs Site',
                          description: 'marketing + guides',
                          icon: 'i-lucide-book-open',
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        },
      ],
    },
    {
      type: 'group',
      label: 'View',
      children: [
        {
          type: 'checkbox',
          label: 'Line Numbers',
          icon: 'i-lucide-list',
          checked: showLineNumbers(),
          onCheckedChange: (checked: boolean) => setShowLineNumbers(checked),
        },
        {
          type: 'checkbox',
          label: 'Minimap',
          icon: 'i-lucide-map',
          checked: showMinimap(),
          onCheckedChange: (checked: boolean) => setShowMinimap(checked),
        },
        {
          type: 'checkbox',
          label: 'Preview Tabs',
          icon: 'i-lucide-panel-top',
          checked: previewTabs(),
          onCheckedChange: (checked: boolean) => setPreviewTabs(checked),
        },
        {
          type: 'checkbox',
          label: 'Auto Save',
          icon: 'i-lucide-save',
          checked: autoSave(),
          onCheckedChange: (checked: boolean) => setAutoSave(checked),
        },
        { type: 'separator' },
        {
          label: 'Theme',
          icon: 'i-lucide-palette',
          children: [
            {
              type: 'group',
              label: 'Appearance',
              children: [
                {
                  label: 'Light',
                  icon: theme() === 'light' ? 'i-lucide-check' : 'i-lucide-sun',
                  onSelect: () => setTheme('light'),
                },
                {
                  label: 'Dark',
                  icon: theme() === 'dark' ? 'i-lucide-check' : 'i-lucide-moon',
                  onSelect: () => setTheme('dark'),
                },
                {
                  label: 'System',
                  icon: theme() === 'system' ? 'i-lucide-check' : 'i-lucide-monitor',
                  onSelect: () => setTheme('system'),
                },
              ],
            },
          ],
        },
      ],
    },
    {
      type: 'group',
      label: 'Panels',
      children: [
        {
          label: 'Toggle Terminal',
          icon: 'i-lucide-square-terminal',
          kbds: ['⌃', '`'],
        },
        {
          label: 'Focus Problems',
          icon: 'i-lucide-triangle-alert',
          kbds: ['⇧', '⌘', 'M'],
        },
      ],
    },
  ])

  return (
    <div class="flex flex-col gap-3">
      <div class="flex flex-wrap gap-3 items-center">
        <DropdownMenu items={editorItems()}>
          <Button variant="outline">Editor menu</Button>
        </DropdownMenu>
      </div>
      <div class="text-sm text-muted-foreground flex flex-wrap gap-4">
        <span>
          Line numbers: <span class="font-medium">{String(showLineNumbers())}</span>
        </span>
        <span>
          Minimap: <span class="font-medium">{String(showMinimap())}</span>
        </span>
        <span>
          Preview tabs: <span class="font-medium">{String(previewTabs())}</span>
        </span>
        <span>
          Auto save: <span class="font-medium">{String(autoSave())}</span>
        </span>
        <span>
          Theme: <span class="font-medium uppercase">{theme()}</span>
        </span>
      </div>
    </div>
  )
}
```

### Project / Release Actions

A heavier project menu with move flows, release actions, mixed-content labels, and destructive project operations.

```tsx
function ProjectReleaseActions() {
  const [lastAction, setLastAction] = createSignal('None')

  const badgeClass =
    'rounded-md b-1 b-border border-border bg-muted px-1.5 py-0.5 font-medium text-[11px] text-foreground'

  const projectItems: DropdownMenuT.Item[] = [
    {
      type: 'group',
      label: (
        <div class="flex gap-2 items-center">
          <span>Project: moraine</span>
          <span class={badgeClass}>main</span>
        </div>
      ),
      children: [
        {
          label: 'New File',
          icon: 'i-lucide-file-plus',
          kbds: ['⌘', 'N'],
          onSelect: () => setLastAction('New file'),
        },
        {
          label: 'New Folder',
          icon: 'i-lucide-folder-plus',
          kbds: ['⇧', '⌘', 'N'],
          onSelect: () => setLastAction('New folder'),
        },
        {
          label: 'Rename Project',
          icon: 'i-lucide-pencil',
          kbds: ['F2'],
          onSelect: () => setLastAction('Rename project'),
        },
        { type: 'separator' },
        {
          label: 'Move To…',
          icon: 'i-lucide-folder-input',
          children: [
            {
              type: 'group',
              label: 'Favorite Folders',
              children: [
                {
                  label: 'src/components',
                  icon: 'i-lucide-folder-open',
                  onSelect: () => setLastAction('Move to src/components'),
                },
                {
                  label: 'src/overlays',
                  icon: 'i-lucide-folder-open',
                  onSelect: () => setLastAction('Move to src/overlays'),
                },
                {
                  label: 'More Destinations',
                  icon: 'i-lucide-more-horizontal',
                  children: [
                    {
                      type: 'group',
                      children: [
                        {
                          label: 'docs/content',
                          icon: 'i-lucide-folder-open',
                          onSelect: () => setLastAction('Move to docs/content'),
                        },
                        {
                          label: 'archive/2025',
                          icon: 'i-lucide-folder-open',
                          onSelect: () => setLastAction('Move to archive/2025'),
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          ],
        },
        {
          label: 'Copy Preview Link',
          icon: 'i-lucide-link',
          kbds: ['⌘', '⇧', 'C'],
          onSelect: () => setLastAction('Copy preview link'),
        },
      ],
    },
    {
      type: 'group',
      label: 'Release',
      children: [
        {
          label: 'Open Pull Request',
          icon: 'i-lucide-git-pull-request-arrow',
          onSelect: () => setLastAction('Open pull request'),
        },
        {
          label: 'Deploy Preview',
          description: 'Build preview for design review',
          icon: 'i-lucide-rocket',
          onSelect: () => setLastAction('Deploy preview'),
        },
        {
          label: (
            <div class="flex gap-2 items-center">
              <span>Ship to Production</span>
              <span class="text-[11px] text-amber-700 font-medium px-1.5 py-0.5 rounded-md bg-amber-100">
                Protected
              </span>
            </div>
          ),
          description: 'Requires review approval',
          icon: <span class="rounded-full bg-emerald-500 size-2 inline-block" />,
          onSelect: () => setLastAction('Ship to production'),
        },
      ],
    },
    {
      type: 'group',
      children: [
        {
          label: 'Archive Project',
          icon: 'i-lucide-archive',
          onSelect: () => setLastAction('Archive project'),
        },
        {
          label: 'Delete Project',
          icon: 'i-lucide-trash-2',
          color: 'destructive',
          onSelect: () => setLastAction('Delete Project'),
        },
      ],
    },
  ]

  return (
    <>
      <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <DropdownMenu items={projectItems}>
          <Button>Project actions</Button>
        </DropdownMenu>
        <p class="text-sm text-muted-foreground">
          Tip: use arrow keys to walk the nested “Move To…” and release sections.
        </p>
      </div>
      <div class="text-sm text-muted-foreground">
        Last action: <span class="font-medium">{lastAction()}</span>
      </div>
    </>
  )
}
```

## API Reference

### Attributes

#### `trigger`

Element users activate to open the menu.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-closed | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |

#### `overlay`

Optional backdrop rendered behind modal menu content.

#### `content`

Positioned menu panel that contains groups, items, and submenus.

##### CSS Variables

| Attribute | Type | Description |
| --- | --- | --- |
| --mo-popper-content-transform-origin | string | CSS custom property exposed by this slot. |

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-placement | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `group`

Section wrapper for related menu items.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `label`

Heading text for a menu group.

#### `separator`

Non-interactive divider between menu sections.

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `item`

Action row inside menu content, including checkbox and radio items.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-selected | string \| undefined | Present when the item is selected. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-checked | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `itemLeading`

Leading icon or visual shown for a menu item.

#### `itemWrapper`

Text column that groups menu item label and description.

#### `itemLabel`

Primary text for a menu item.

#### `itemDescription`

Supporting text for a menu item.

#### `itemTrailing`

Trailing region for shortcuts, submenu arrows, or selection indicators.

#### `itemKbds`

Container for keyboard shortcut hints at the end of a menu item.

#### `itemIndicator`

Checked or selected-state indicator for checkbox and radio menu items.

#### `itemSub`

Indicator shown when a menu item opens a submenu.

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checkedIcon | IconT.Name | icon-check | Icon used for checked checkbox items. |
| children | JSX.Element | — | Trigger content used to open the dropdown menu. |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | (OverlayMenuSharedClasses & DropdownMenuT.Classes) \| undefined | — | Slot class overrides for menu sections. |
| contentBottom | OverlayMenuContentSlot \| undefined | — | Content rendered after the resolved item groups. |
| contentProps | ElementProps<HTMLDivElement> \| undefined | — | Additional attributes for each menu layer content element. |
| contentTop | OverlayMenuContentSlot \| undefined | — | Content rendered before the resolved item groups. |
| defaultOpen | boolean \| undefined | false | Initial open state when the component is uncontrolled. |
| disabled | boolean \| undefined | false | Whether trigger interactions should be ignored. |
| gutter | number \| undefined | 0 | Gap between the anchor and the content. |
| id | string \| undefined | — | Unique base id used to derive trigger and content ids. |
| itemProps | ((props: DropdownMenuT.ItemRenderProps) => ElementProps<HTMLDivElement> \| undefined) \| undefined | — | Additional attributes for an interactive menu item. |
| itemRender | ComponentOrElement<DropdownMenuT.ItemRenderProps> \| undefined | — | Custom renderer for individual items. |
| items | DropdownMenuT.Item[] \| undefined | — | Items rendered in the menu body. |
| onClick | JSX.EventHandlerUnion<HTMLSpanElement, MouseEvent> \| undefined | — | Root trigger click handler. |
| onKeyDown | JSX.EventHandlerUnion<HTMLSpanElement, KeyboardEvent> \| undefined | — | Root trigger keyboard handler. |
| onOpenChange | ((open: boolean) => void) \| undefined | — | Called whenever the menu requests an open state change. |
| open | boolean \| undefined | — | Controlled open state of the menu. |
| overflowPadding | number \| undefined | 4 | Padding applied to the overflow area when calculating the menu's position. |
| placement | OverlayMenuPlacement \| undefined | — | Preferred content placement relative to the trigger or anchor point. |
| preventScroll | boolean \| undefined | true | Whether body scroll should be locked while the menu is open. |
| ref | JSX.HTMLElementTags["span"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| size | "sm" \| "md" \| "lg" \| undefined | md | Menu item size variant. |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | (OverlayMenuSharedStyles & DropdownMenuT.Styles) \| undefined | — | Slot style overrides for menu sections. |
| submenuIcon | IconT.Name | icon-chevron-right | Icon used for submenu trigger items. |

### Items

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| checked | boolean \| undefined | — | Controlled checked state for checkbox and radio items. |
| children | DropdownMenuT.Item[] \| undefined | — | Nested menu items for creating submenus. |
| color | NonNullable<"default" \| "destructive" \| undefined> \| undefined | — | Color theme variant for the menu item. |
| defaultChecked | boolean \| undefined | — | Initial checked state for uncontrolled checkbox and radio items. |
| defaultOpen | boolean \| undefined | — | Initial open state for submenus. |
| description | JSX.Element | — | Secondary description text displayed below the label. |
| disabled | boolean \| undefined | false | Whether the item is non-interactive. |
| group | string \| undefined | — | Radio group identifier. Radio items with the same group are mutually exclusive. |
| icon | IconT.Name | — | Icon name or custom element to display at the start of the item. |
| kbds | string[] \| undefined | — | Array of keyboard shortcuts to display as keys. |
| label | JSX.Element | — | Primary label text or element. |
| onCheckedChange | ((checked: boolean) => void) \| undefined | — | Event handler called when a checkbox item's state changes. |
| onSelect | (() => void) \| undefined | — | Event handler called when the item is activated. |
| onValueChange | ((value: string) => void) \| undefined | — | Event handler called when a radio item is selected. |
| open | boolean \| undefined | — | Controlled open state for submenus. |
| type | OverlayMenuItemType \| undefined | item | The type of menu item to render. |
| value | string \| undefined | — | Radio item value reported by onValueChange and used for grouped selection. |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-checked | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-controls | boolean \| string \| undefined | References the controlled element while the related content is mounted. |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-expanded | boolean \| string \| undefined | Indicates whether the controlled content is expanded. |
| aria-haspopup | boolean \| string \| undefined | Accessibility attribute forwarded by the rendered component. |
| aria-hidden | boolean \| string \| undefined | Hides decorative content from assistive technology. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-closed | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-expanded | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-highlighted | string \| undefined | Present when the item is highlighted by pointer or keyboard navigation. |
| data-placement | string \| undefined | State or slot attribute exposed for styling hooks and selectors. |
| data-selected | string \| undefined | Present when the item is selected. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
