elements
Demos UI Feed Discuss Install
Style System
  • Style
  • Themes
  • Behaviors
Forms
  • Button
  • ButtonGroup
  • Checkbox
  • ColorPicker
  • Combobox
  • Field
  • FileUpload
  • Input
  • InputGroup
  • InputOTP
  • Label
  • NativeSelect
  • NumberInput
  • PasswordInput
  • RadioGroup
  • Select
  • Slider
  • Switch
  • TagsInput
  • Textarea
  • Toggle
  • ToggleGroup
Overlays
  • AlertDialog
  • Command
  • ContextMenu
  • Dialog
  • Drawer
  • DropdownMenu
  • HoverCard
  • Menubar
  • NavigationMenu
  • Popover
  • Sheet
  • Toast
  • Tooltip
Display
  • Accordion
  • Alert
  • AspectRatio
  • Avatar
  • Badge
  • Breadcrumb
  • Card
  • Collapsible
  • Empty
  • Item
  • Kbd
  • Pagination
  • Progress
  • Separator
  • Skeleton
  • Spinner
  • Table
  • Tabs
  • Tree
Layout
  • Carousel
  • Resizable
  • ScrollArea
  • Sidebar
Dates
  • Calendar
  • DatePicker
  • DateRangePicker
  • TimePicker
Data
  • AreaChart
  • BarChart
  • Chart
  • DataTable
  • Kanban
  • LineChart
  • PieChart
  • RadarChart
  • RadialChart
  • VirtualList
Chat
  • Attachment
  • Message
  • MessageList
  • PromptInput
Elements UI / Overlays / DropdownMenu

DropdownMenu

A menu of actions from a button, with submenus, checkbox and radio items.

Account menu

Labels, groups, shortcuts, a disabled item and nested submenus. Opened by pointer, focus is on the menu; by keyboard, on the first item.

selected: none
Code
<DropdownMenu align="start">
  <Button slot="trigger">Open</Button>
  <DropdownMenuGroup>
    <DropdownMenuLabel>My Account</DropdownMenuLabel>
    <DropdownMenuItem onSelect={() => go("profile")}>
      Profile <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
    </DropdownMenuItem>
    <DropdownMenuItem>Billing <DropdownMenuShortcut>⌘B</DropdownMenuShortcut></DropdownMenuItem>
    <DropdownMenuItem>Settings <DropdownMenuShortcut>⌘S</DropdownMenuShortcut></DropdownMenuItem>
  </DropdownMenuGroup>
  <DropdownMenuSeparator />
  <DropdownMenuGroup>
    <DropdownMenuItem>Team</DropdownMenuItem>
    <DropdownMenuSub label="Integrations" disabled>...</DropdownMenuSub>
    <DropdownMenuSub label="Invite users">
      <DropdownMenuItem>Email</DropdownMenuItem>
      <DropdownMenuItem>Message</DropdownMenuItem>
      <DropdownMenuSeparator />
      <DropdownMenuSub label="More">
        <DropdownMenuItem>Calendar</DropdownMenuItem>
        <DropdownMenuItem>Chat</DropdownMenuItem>
      </DropdownMenuSub>
    </DropdownMenuSub>
    <DropdownMenuItem>New Team <DropdownMenuShortcut>⌘+T</DropdownMenuShortcut></DropdownMenuItem>
  </DropdownMenuGroup>
  <DropdownMenuSeparator />
  <DropdownMenuItem>GitHub</DropdownMenuItem>
  <DropdownMenuItem>Support</DropdownMenuItem>
  <DropdownMenuItem disabled>API</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuItem>Log out <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut></DropdownMenuItem>
</DropdownMenu>

Usage

import {
  DropdownMenu, DropdownMenuItem, DropdownMenuCheckboxItem, DropdownMenuRadioGroup, DropdownMenuRadioItem,
  DropdownMenuLabel, DropdownMenuGroup, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub,
} from "@elements/ui/dropdown-menu";

<DropdownMenu>
  <Button slot="trigger">Open</Button>
  <DropdownMenuLabel>My Account</DropdownMenuLabel>
  <DropdownMenuItem onSelect={() => go("/profile")}>
    Profile <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
  </DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuCheckboxItem checked={view.statusBar}>Status bar</DropdownMenuCheckboxItem>
  <DropdownMenuRadioGroup value={view.position}>
    <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
  <DropdownMenuSub label="More tools">
    <DropdownMenuItem>Save page as</DropdownMenuItem>
  </DropdownMenuSub>
</DropdownMenu>

Examples

Icons and destructive

Icons size from the theme. destructive marks an action that cannot be undone. A rich sub trigger goes in its trigger slot.

Code
<DropdownMenu>
  <Button slot="trigger" icon label="More"><Icon name="more-horizontal" /></Button>
  <DropdownMenuItem><Icon name="user" /> Profile</DropdownMenuItem>
  <DropdownMenuItem><Icon name="settings" /> Settings</DropdownMenuItem>
  <DropdownMenuSub>
    <span slot="trigger"><Icon name="user-plus" /> Invite</span>
    <DropdownMenuItem><Icon name="mail" /> Email</DropdownMenuItem>
  </DropdownMenuSub>
  <DropdownMenuSeparator />
  <DropdownMenuItem variant="destructive"><Icon name="trash" /> Delete</DropdownMenuItem>
</DropdownMenu>

Checkbox items

checked is two-way and may be indeterminate. The check column is reserved, so toggling moves nothing. closeOnSelect={false} keeps a checklist open.

status: true · activity: indeterminate · email: true · sms: false
Code
<DropdownMenu>
  <Button slot="trigger">View</Button>
  <DropdownMenuLabel>Appearance</DropdownMenuLabel>
  <DropdownMenuSeparator />
  <DropdownMenuCheckboxItem checked={view.statusBar}>Status Bar</DropdownMenuCheckboxItem>
  <DropdownMenuCheckboxItem checked={view.activityBar}>Activity Bar</DropdownMenuCheckboxItem>
  <DropdownMenuCheckboxItem checked={view.panel} disabled>Panel</DropdownMenuCheckboxItem>
</DropdownMenu>

<!-- a checklist that stays open -->
<DropdownMenuCheckboxItem checked={view.email} closeOnSelect={false}>Email</DropdownMenuCheckboxItem>

Radio group

value is two-way on the group; each radio item carries its own value.

position: bottom
Code
<DropdownMenu>
  <Button slot="trigger">Panel position</Button>
  <DropdownMenuLabel>Panel Position</DropdownMenuLabel>
  <DropdownMenuSeparator />
  <DropdownMenuRadioGroup value={state.position}>
    <DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
</DropdownMenu>

Inset

inset on an item, label or sub trigger keeps the indicator column, so its text lines up with checkbox and radio rows.

Code
<DropdownMenu>
  <Button slot="trigger">Inset</Button>
  <DropdownMenuLabel inset>Editor</DropdownMenuLabel>
  <DropdownMenuItem inset>Format document</DropdownMenuItem>
  <DropdownMenuSub inset label="Go to">...</DropdownMenuSub>
  <DropdownMenuSeparator />
  <DropdownMenuCheckboxItem checked={view.minimap}>Minimap</DropdownMenuCheckboxItem>
</DropdownMenu>

Avatar trigger

Any element can be the trigger. An account switcher aligned to the end.

Code
<DropdownMenu align="end">
  <button slot="trigger" class="button is-ghost is-icon" aria-label="Account">
    <Avatar src="/avatar.png" fallback="CM" size="sm" />
  </button>
  <DropdownMenuLabel>chris@elements.dev</DropdownMenuLabel>
  ...
</DropdownMenu>

Controlled and events

open and a submenu's open are two-way. onOpenChange fires for changes the menu makes; onOpenChangeComplete fires once the open or close animation has finished. onSelect with preventDefault() keeps the menu open.

open: false · sub: false · no changes · complete: none
Code
<Button onclick={() => state.open = true}>Open from outside</Button>

<DropdownMenu open={state.open}
              onOpenChange={(open) => log(open)}
              onOpenChangeComplete={(open) => log(open ? "open complete" : "closed complete")}>
  <Button slot="trigger">Controlled</Button>
  <DropdownMenuItem onSelect={(e) => e.preventDefault()}>Stays open</DropdownMenuItem>
  <DropdownMenuSub label="Submenu" open={state.subOpen} onOpenChangeComplete={(open) => log(open)}>...</DropdownMenuSub>
</DropdownMenu>

Non-modal

modal={false} leaves the page usable: a press outside closes the menu and lands where it was aimed.

outside: none
Code
<DropdownMenu modal={false}
              onPointerDownOutside={(e) => log("pointer")}
              onFocusOutside={(e) => log("focus")}
              onInteractOutside={(e) => log("interact")}>
  <Button slot="trigger">Non-modal</Button>
  ...
</DropdownMenu>

Open on hover

openOnHover opens the menu when the pointer rests on the trigger for delay ms, without moving focus, and closes it closeDelay ms after the pointer leaves the trigger and the menu. A press on the trigger keeps it open. Keyboard and touch work as usual.

Code
<DropdownMenu openOnHover delay={100} closeDelay={0}>
  <Button slot="trigger" variant="outline">Open on hover</Button>
  <DropdownMenuItem>Profile</DropdownMenuItem>
  <DropdownMenuSub label="Invite users">...</DropdownMenuSub>
  ...
</DropdownMenu>

Hover highlight off

highlightItemOnHover={false} leaves the highlight to the keyboard: the pointer resting on a row does not highlight it, so CSS :hover and the highlighted row can be styled apart. A click still chooses, and a submenu still opens under a resting pointer.

Code
<DropdownMenu highlightItemOnHover={false}>
  <Button slot="trigger" variant="outline">No hover highlight</Button>
  <DropdownMenuItem>One</DropdownMenuItem>
  <DropdownMenuItem>Two</DropdownMenuItem>
  <DropdownMenuSub label="More">...</DropdownMenuSub>
</DropdownMenu>

Several triggers, one menu

triggers names buttons elsewhere on the page that open this one menu, each against itself. onTriggerChange runs first, so the menu shows that button's rows. With the menu open, pressing another of its triggers, or Enter or ArrowDown on one, moves it there: the popup slides and resizes to the new rows, which fade in, and data-activation-direction says which way it went. With open bound, activeTrigger names the trigger it opens from when code opens it.

menu: file · changes: 0
Code
<div role="toolbar" aria-label="Editor">
  <Button variant="ghost" attrs={{ "data-toolbar-menu": true, "data-menu": "file" }}>File</Button>
  <Button variant="ghost" attrs={{ "data-toolbar-menu": true, "data-menu": "edit" }}>Edit</Button>
  <Button variant="ghost" attrs={{ "data-toolbar-menu": true, "data-menu": "view" }}>View</Button>
</div>
<DropdownMenu
  triggers="[data-toolbar-menu]"
  align="start"
  open={state.open}
  activeTrigger={`[data-menu=${state.menu}]`}
  onTriggerChange={(t) => pick(state, t)}
>
  <DropdownMenuItem e:for={row of ROWS[state.menu]}>{row}</DropdownMenuItem>
</DropdownMenu>
<Button onclick={() => openOn(state, "view")}>Open from View</Button>

Placement

side and align pick the preferred spot; the menu flips to stay on screen. arrow points at the trigger. The top menu ignores Escape (onEscapeKeyDown prevented); the right one leaves focus alone on close (onCloseAutoFocus prevented). avoidCollisions={false} keeps a side even off screen. arrowWidth and arrowHeight size the arrow. anchor places the menu against another element than its trigger.

Anchor
Code
<DropdownMenu side="top" align="start" arrow onEscapeKeyDown={(e) => e.preventDefault()}>...</DropdownMenu>
<DropdownMenu side="right" sideOffset={8} onCloseAutoFocus={(e) => e.preventDefault()}>...</DropdownMenu>
<DropdownMenu side="bottom" align="end">...</DropdownMenu>
<DropdownMenu side="top" align="start" alignOffset={6} avoidCollisions={false}>...</DropdownMenu>
<DropdownMenu arrow arrowWidth={20} arrowHeight={10}>...</DropdownMenu>

<!-- placed against another element instead of the trigger -->
<div id="toolbar">...</div>
<DropdownMenu anchor="#toolbar" align="start">...</DropdownMenu>

Collisions

collisionBoundary names a box whose edges count like the viewport's, so this menu flips up inside it. sticky="always" keeps a menu in view when its trigger scrolls away. arrowPadding keeps the arrow off the menu's corner.

Code
<div class="boundary">
  <DropdownMenu collisionBoundary=".boundary">...</DropdownMenu>
</div>
<DropdownMenu modal={false} side="right" sticky="always">...</DropdownMenu>
<DropdownMenu arrow align="start" alignOffset={30} arrowPadding={12}>
  <Button slot="trigger" icon label="More"><Icon name="more-vertical" /></Button>
  ...
</DropdownMenu>

Hide when detached

hideWhenDetached hides the menu while its trigger is scrolled out of view, here inside a scroll box. Non-modal, so the box still scrolls.

scroll this box
Code
<div class="scroller">
  <DropdownMenu modal={false} hideWhenDetached>
    <Button slot="trigger">Scroll me away</Button>
    ...
  </DropdownMenu>
</div>

Links

href renders a link row: Enter and click follow it.

link target
Code
<DropdownMenu>
  <Button slot="trigger">Go to</Button>
  <DropdownMenuItem href="/ui/dialog">Dialog</DropdownMenuItem>
  <DropdownMenuItem href="/ui/popover">Popover</DropdownMenuItem>
</DropdownMenu>

Long list

The menu scrolls inside the room it has. loop wraps the arrow keys; typing a name jumps to it.

Code
<DropdownMenu loop>
  <Button slot="trigger">Assign</Button>
  <DropdownMenuItem e:for={name of people}>{name}</DropdownMenuItem>
</DropdownMenu>
<DropdownMenu arrow>
  <Button slot="trigger" variant="outline">Assign, arrow</Button>
  <DropdownMenuItem e:for={name of people}>{name}</DropdownMenuItem>
</DropdownMenu>

Horizontal

orientation="horizontal" lays the items in a row: ArrowLeft and ArrowRight move between them and ArrowDown opens a submenu.

Code
<DropdownMenu orientation="horizontal" align="start">
  <Button slot="trigger">Format</Button>
  <DropdownMenuItem>Bold</DropdownMenuItem>
  <DropdownMenuItem>Italic</DropdownMenuItem>
  <DropdownMenuSeparator orientation="vertical" />
  <DropdownMenuSub label="Align">
    <DropdownMenuItem>Left</DropdownMenuItem>
    <DropdownMenuItem>Center</DropdownMenuItem>
  </DropdownMenuSub>
</DropdownMenu>

<!-- a horizontal submenu in a vertical menu: ArrowUp closes it -->
<DropdownMenuSub label="Align" orientation="horizontal">...</DropdownMenuSub>

Backdrop and positioning

backdrop renders a layer under the menu that dims the page; a press on it closes the menu. positionMethod="absolute" places the menu in page coordinates, and disableAnchorTracking places it once when it opens: it stays put if the trigger moves, and is placed again on a scroll or resize.

Code
<DropdownMenu backdrop>
  <Button slot="trigger">Dim the page</Button>
  ...
</DropdownMenu>

<!-- placed once with position: absolute; it stays put if the trigger moves -->
<DropdownMenu positionMethod="absolute" disableAnchorTracking>...</DropdownMenu>

Filtering

An input row narrows the items as you type; case, accents and punctuation do not count. The popup is a dialog holding the searchbox and the menu list (DropdownMenuList). Focus stays in the input while ArrowDown and ArrowUp move the highlight; Enter chooses. autoHighlight keeps a match highlighted and the arrows wrap, the clear button empties the input, and the empty row shows when nothing matches. A submenu filters with its own input. DropdownMenuList may be left out: the rows beside the input still form the menu list. A menu opened by hover, touch or pen leaves the input unfocused, so no on-screen keyboard appears. filter picks the match: contains (the default), startsWith, endsWith, or a function of the row's text and the query. locale folds case in a language's rules: in Turkish, i matches İstanbul and İzmir but not Irmak, whose I is the capital of dotless ı.

status: Todo · query: none · changes: 0
Code
<DropdownMenu align="start">
  <Button slot="trigger">Status</Button>
  <DropdownMenuInput value={state.query} placeholder="Change status..." autoHighlight clearButton />
  <DropdownMenuList>
    <DropdownMenuGroup>
      <DropdownMenuLabel>Status</DropdownMenuLabel>
      <DropdownMenuItem>Backlog</DropdownMenuItem>
      <DropdownMenuItem>Todo</DropdownMenuItem>
      <DropdownMenuItem>In Progress</DropdownMenuItem>
    </DropdownMenuGroup>
  </DropdownMenuList>
  <DropdownMenuEmpty>No status found.</DropdownMenuEmpty>
</DropdownMenu>

<!-- A submenu filters with its own input; the parent is not filtered. -->
<DropdownMenu align="start">
  <Button slot="trigger">Labels</Button>
  <DropdownMenuItem>Create label</DropdownMenuItem>
  <DropdownMenuSub label="Apply label">
    <DropdownMenuInput placeholder="Filter labels..." autoHighlight />
    <DropdownMenuList>
      <DropdownMenuItem>Bug</DropdownMenuItem>
      <DropdownMenuItem>Feature</DropdownMenuItem>
    </DropdownMenuList>
  </DropdownMenuSub>
</DropdownMenu>

<!-- Opened by hover, touch or pen, the input does not take focus.
     Without DropdownMenuList the rows still form the menu list. -->
<DropdownMenu openOnHover align="start">
  <Button slot="trigger">Hover</Button>
  <DropdownMenuInput placeholder="Filter..." />
  <DropdownMenuItem>Bug</DropdownMenuItem>
  <DropdownMenuItem>Feature</DropdownMenuItem>
</DropdownMenu>
<!-- startsWith, endsWith or a function (itemText, query) => boolean -->
<DropdownMenuInput filter="startsWith" placeholder="Starts with..." />

<!-- case folds in Turkish rules: i finds İstanbul, not Irmak -->
<DropdownMenuInput locale="tr" placeholder="Şehir ara..." />

Inside a form

A menu inside a form never submits it: the trigger is a type=button, and Enter in a filter input chooses the highlighted row or does nothing.

submits: 0
Code
<form>
  <DropdownMenu>
    <Button slot="trigger" variant="outline">Status</Button>
    <DropdownMenuInput placeholder="Change status..." />
    <DropdownMenuList>
      <DropdownMenuItem e:for={s of statuses}>{s}</DropdownMenuItem>
    </DropdownMenuList>
    <DropdownMenuEmpty>No status found.</DropdownMenuEmpty>
  </DropdownMenu>
  <Button type="submit">Save</Button>
</form>

Submenu and group options

A submenu can open only on click (openOnHover={false}), wait before it closes (closeDelay), keep Escape for itself, or close only itself on Escape and return to its row (closeParentOnEsc={false}, as APG). A radio group can be disabled as a whole. onItemHighlighted reports the highlighted row; on a submenu it reports that submenu's rows. A submenu hears presses and focus outside it (onPointerDownOutside, onFocusOutside, onInteractOutside), and preventDefault() in onFocusOutside keeps it open when focus moves to another row. Several triggers for the same items are several menus.

highlighted: none · theme: light · selected: none submenu highlighted: none · outside the submenu: none
Code
<DropdownMenu onItemHighlighted={(item) => show(item?.textContent)}>
  <Button slot="trigger">Options</Button>
  <DropdownMenuSub label="Click to open" openOnHover={false}>...</DropdownMenuSub>
  <DropdownMenuSub label="Slow to close" delay={0} closeDelay={400} loop>...</DropdownMenuSub>
  <DropdownMenuSub label="Keeps Escape" sideOffset={6} onEscapeKeyDown={(e) => e.preventDefault()}>...</DropdownMenuSub>
  <DropdownMenuSub label="Escape steps back" closeParentOnEsc={false}>...</DropdownMenuSub>
  <DropdownMenuItem textValue="Zoom"><Icon name="search" /> 125%</DropdownMenuItem>
  <DropdownMenuSeparator />
  <DropdownMenuRadioGroup value={state.theme} disabled>
    <DropdownMenuRadioItem value="light">Light</DropdownMenuRadioItem>
    <DropdownMenuRadioItem value="dark">Dark</DropdownMenuRadioItem>
  </DropdownMenuRadioGroup>
</DropdownMenu>

<!-- a submenu hears presses and focus outside it, and reports its own highlighted row -->
<DropdownMenuSub label="Reports"
  onPointerDownOutside={(e) => log("press")}
  onFocusOutside={(e) => log("focus")}
  onInteractOutside={(e) => log("interact")}
  onItemHighlighted={(item) => show(item?.textContent)}>...</DropdownMenuSub>
<DropdownMenuSub label="Stays open" onFocusOutside={(e) => e.preventDefault()}>...</DropdownMenuSub>

<!-- several triggers: one menu each, sharing its items -->
<DropdownMenu e:for={doc of docs}>
  <Button slot="trigger">{doc.name}</Button>
  <DropdownMenuItem onSelect={() => open(doc)}>Open</DropdownMenuItem>
</DropdownMenu>

Right to left

Under dir=rtl the submenu opens to the left, ArrowLeft opens it and ArrowRight closes it.

Code
<div dir="rtl">
  <DropdownMenu align="start">
    <Button slot="trigger">فتح</Button>
    <DropdownMenuItem>الملف الشخصي</DropdownMenuItem>
    <DropdownMenuSub label="دعوة">...</DropdownMenuSub>
  </DropdownMenu>
</div>

Inside a dialog

A menu inside a modal dialog: Escape and a press outside the menu close only the menu, and the dialog stays open and usable.

Code
<Dialog title="Share">
  <Button slot="trigger">Share</Button>
  <DropdownMenu>
    <Button slot="trigger">Permission</Button>
    <DropdownMenuItem>Can view</DropdownMenuItem>
    <DropdownMenuItem>Can edit</DropdownMenuItem>
  </DropdownMenu>
</Dialog>
Reference DropdownMenu in the manual Every part, attribute, slot, key and data attribute.
Prev DrawerNext HoverCard
Product
Manual Pricing Compare
Company
History Contact
Legal
Privacy Policy Acceptable Use Terms License Agreement
© Elements Dev Corporation 2026