Dropdown
A dropdown displays a list of actions or options that a user can choose
Usage
import { Dropdown } from '@hareui/vue'<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'const items: DropdownEntry[] = [ { value: 'new-file', label: 'New file' }, { value: 'copy-link', label: 'Copy link' }, { value: 'edit-file', label: 'Edit file' }, { value: 'delete-file', label: 'Delete file', variant: 'danger' },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Actions </Button> </Dropdown></template>Anatomy
A single Dropdown component owns the whole structure. The default slot is the trigger (as-child, merged directly onto its own root element). The menu is built from the items array: items, { type: 'section' } groups with a header, { type: 'separator' } dividers and submenus (an item with nested items). Named slots customize the parts of each item.
<script setup lang="ts">
import type { DropdownEntry } from '@hareui/vue'
import { Button, Dropdown } from '@hareui/vue'
const items: DropdownEntry[] = [
{ type: 'section', label: 'Actions', items: [
{ value: 'new', label: 'New file', description: 'Create a new file', kbd: { keys: ['command'], content: 'N' } },
] },
{ type: 'separator' },
{ value: 'share', label: 'Share', items: [{ value: 'email', label: 'Email' }] },
{ value: 'delete', label: 'Delete', variant: 'danger' },
]
</script>
<template>
<Dropdown :items="items" @action="onAction">
<Button>Open</Button>
<template #content-top>Content above the menu</template>
<template #item-leading="{ item }">Icon</template>
<template #indicator="{ selected }">Custom indicator</template>
<template #submenu-indicator="{ item }">Custom chevron</template>
</Dropdown>
</template>Examples
With Icons
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { Icon } from '@iconify/vue'const items: DropdownEntry[] = [ { value: 'new-file', label: 'New file', icon: 'gravity-ui:square-plus', kbd: { keys: ['command'], content: 'N' } }, { value: 'open-file', label: 'Open file', icon: 'gravity-ui:folder-open', kbd: { keys: ['command'], content: 'O' } }, { value: 'save-file', label: 'Save file', icon: 'gravity-ui:floppy-disk', kbd: { keys: ['command'], content: 'S' } }, { value: 'delete-file', label: 'Delete file', icon: 'gravity-ui:trash-bin', variant: 'danger', kbd: { keys: ['command', 'shift'], content: 'D' } },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Actions </Button> <template #item-leading="{ item }"> <Icon class="size-4 shrink-0" :class="item.variant === 'danger' ? 'text-danger' : 'text-muted'" :icon="item.icon" /> </template> </Dropdown></template>With Descriptions
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { Icon } from '@iconify/vue'const items: DropdownEntry[] = [ { value: 'new-file', label: 'New file', description: 'Create a new file', icon: 'gravity-ui:square-plus', kbd: { keys: ['command'], content: 'N' } }, { value: 'open-file', label: 'Open file', description: 'Open an existing file', icon: 'gravity-ui:folder-open', kbd: { keys: ['command'], content: 'O' } }, { value: 'save-file', label: 'Save file', description: 'Save the current file', icon: 'gravity-ui:floppy-disk', kbd: { keys: ['command'], content: 'S' } }, { value: 'delete-file', label: 'Delete file', description: 'Move to trash', icon: 'gravity-ui:trash-bin', variant: 'danger', kbd: { keys: ['command', 'shift'], content: 'D' } },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Actions </Button> <template #item-leading="{ item }"> <div class="flex h-8 items-start justify-center pt-px"> <Icon class="size-4 shrink-0" :class="item.variant === 'danger' ? 'text-danger' : 'text-muted'" :icon="item.icon" /> </div> </template> </Dropdown></template>With Disabled Items
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { Icon } from '@iconify/vue'const items: DropdownEntry[] = [ { type: 'section', label: 'Actions', items: [ { value: 'new-file', label: 'New file', description: 'Create a new file', icon: 'gravity-ui:square-plus', kbd: { keys: ['command'], content: 'N' } }, { value: 'edit-file', label: 'Edit file', description: 'Make changes', icon: 'gravity-ui:pencil', kbd: { keys: ['command'], content: 'E' } }, ], }, { type: 'separator' }, { type: 'section', label: 'Danger zone', items: [ { value: 'delete-file', label: 'Delete file', description: 'Move to trash', icon: 'gravity-ui:trash-bin', variant: 'danger', disabled: true, kbd: { keys: ['command', 'shift'], content: 'D' } }, ], },]</script><template> <Dropdown class="min-w-[220px]" :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" icon-only variant="secondary"> <Icon class="outline-none" height="16" icon="gravity-ui:bars" width="16" /> </Button> <template #item-leading="{ item }"> <div class="flex h-8 items-start justify-center pt-px"> <Icon class="size-4 shrink-0" :class="item.variant === 'danger' ? 'text-danger' : 'text-muted'" :icon="item.icon" /> </div> </template> </Dropdown></template>With Sections
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { Icon } from '@iconify/vue'const items: DropdownEntry[] = [ { type: 'section', label: 'Actions', items: [ { value: 'new-file', label: 'New file', description: 'Create a new file', icon: 'gravity-ui:square-plus', kbd: { keys: ['command'], content: 'N' } }, { value: 'edit-file', label: 'Edit file', description: 'Make changes', icon: 'gravity-ui:pencil', kbd: { keys: ['command'], content: 'E' } }, ], }, { type: 'separator' }, { type: 'section', label: 'Danger zone', items: [ { value: 'delete-file', label: 'Delete file', description: 'Move to trash', icon: 'gravity-ui:trash-bin', variant: 'danger', kbd: { keys: ['command', 'shift'], content: 'D' } }, ], },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" icon-only variant="secondary"> <Icon class="outline-none" height="16" icon="gravity-ui:ellipsis-vertical" width="16" /> </Button> <template #item-leading="{ item }"> <div class="flex h-8 items-start justify-center pt-px"> <Icon class="size-4 shrink-0" :class="item.variant === 'danger' ? 'text-danger' : 'text-muted'" :icon="item.icon" /> </div> </template> </Dropdown></template>With Multiple Selection
<script setup lang="ts">import type { DropdownEntry, DropdownValue } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'const selected = ref<DropdownValue[]>(['apple'])const items: DropdownEntry[] = [ { type: 'section', label: 'Select a fruit', items: [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ], }, { value: 'orange', label: 'Orange' }, { value: 'pear', label: 'Pear' },]</script><template> <Dropdown v-model="selected" class="min-w-[256px]" :items="items" selection-mode="multiple"> <Button aria-label="Menu" variant="secondary"> Preferred Fruits </Button> </Dropdown></template>Controlled
Selected: bold
<script setup lang="ts">import type { DropdownEntry, DropdownValue } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'const selected = ref<DropdownValue[]>(['bold'])const items: DropdownEntry[] = [ { value: 'bold', label: 'Bold' }, { value: 'italic', label: 'Italic' }, { value: 'underline', label: 'Underline' },]</script><template> <div class="flex min-w-sm flex-col items-center justify-center gap-4"> <p class="text-sm text-muted"> Selected: {{ selected.length > 0 ? selected.join(', ') : 'None' }} </p> <Dropdown v-model="selected" :items="items" selection-mode="multiple"> <Button aria-label="Menu" variant="secondary"> Actions </Button> </Dropdown> </div></template>Controlled Open State
Dropdown is: closed
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'const open = ref(false)const items: DropdownEntry[] = [ { value: 'new-file', label: 'New file' }, { value: 'open-file', label: 'Open file' }, { value: 'save-file', label: 'Save file' }, { value: 'delete-file', label: 'Delete file', variant: 'danger' },]</script><template> <div class="flex min-w-sm flex-col items-center justify-center gap-4"> <p class="text-sm text-muted"> Dropdown is: <strong>{{ open ? 'open' : 'closed' }}</strong> </p> <Dropdown v-model:open="open" :items="items"> <Button aria-label="Menu" variant="secondary"> Actions </Button> </Dropdown> </div></template>With Single Selection
<script setup lang="ts">import type { DropdownEntry, DropdownValue } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'const selected = ref<DropdownValue[]>(['apple'])const items: DropdownEntry[] = [ { type: 'section', label: 'Select a fruit', items: [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ], }, { value: 'orange', label: 'Orange' }, { value: 'pear', label: 'Pear' },]</script><template> <Dropdown v-model="selected" class="min-w-[256px]" :items="items" selection-mode="single"> <Button aria-label="Menu" variant="secondary"> Fruit </Button> </Dropdown></template>Single With Custom Indicator
<script setup lang="ts">import type { DropdownEntry, DropdownValue } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'const selected = ref<DropdownValue[]>(['apple'])const items: DropdownEntry[] = [ { type: 'section', label: 'Select a fruit', items: [ { value: 'apple', label: 'Apple' }, { value: 'banana', label: 'Banana' }, { value: 'cherry', label: 'Cherry' }, ], }, { value: 'orange', label: 'Orange' }, { value: 'pear', label: 'Pear' },]</script><template> <Dropdown v-model="selected" class="min-w-[256px]" :items="items" selection-mode="single"> <Button aria-label="Menu" variant="secondary"> Fruits </Button> <template #indicator="{ selected: isSelected }"> <svg v-if="isSelected" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"> <path class="text-accent-soft-foreground" clip-rule="evenodd" d="M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14m3.1-8.55a.75.75 0 1 0-1.2-.9L7.419 8.858L6.03 7.47a.75.75 0 0 0-1.06 1.06l2 2a.75.75 0 0 0 1.13-.08z" fill="currentColor" fill-rule="evenodd" /> </svg> </template> </Dropdown></template>With Section Level Selection
A section with its own selectionMode selects independently of the rest of the menu: re-selecting in a single section only replaces that section's value. Every section's selected values live in the one v-model array.
<script setup lang="ts">import type { DropdownEntry, DropdownValue } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { ref } from 'vue'// One `v-model` holds every section's selection: "Text Style" is multiple, "Text Alignment" single.const selected = ref<DropdownValue[]>(['bold', 'italic', 'left'])const items: DropdownEntry[] = [ { type: 'section', label: 'Actions', items: [ { value: 'cut', label: 'Cut', kbd: { keys: ['command'], content: 'X' } }, { value: 'copy', label: 'Copy', kbd: { keys: ['command'], content: 'C' } }, { value: 'paste', label: 'Paste', kbd: { keys: ['command'], content: 'U' } }, ], }, { type: 'separator' }, { type: 'section', label: 'Text Style', selectionMode: 'multiple', items: [ { value: 'bold', label: 'Bold', kbd: { keys: ['command'], content: 'B' } }, { value: 'italic', label: 'Italic', kbd: { keys: ['command'], content: 'I' } }, { value: 'underline', label: 'Underline', kbd: { keys: ['command'], content: 'U' } }, ], }, { type: 'separator' }, { type: 'section', label: 'Text Alignment', selectionMode: 'single', indicator: 'dot', items: [ { value: 'left', label: 'Left', kbd: { keys: ['alt'], content: 'A' } }, { value: 'center', label: 'Center', kbd: { keys: ['alt'], content: 'H' } }, { value: 'right', label: 'Right', kbd: { keys: ['alt'], content: 'D' } }, ], },]</script><template> <Dropdown v-model="selected" class="min-w-[256px]" :items="items"> <Button aria-label="Menu" variant="secondary"> Styles </Button> </Dropdown></template>With Keyboard Shortcuts
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'const items: DropdownEntry[] = [ { value: 'new', label: 'New', kbd: { keys: ['command'], content: 'N' } }, { value: 'open', label: 'Open', kbd: { keys: ['command'], content: 'O' } }, { value: 'save', label: 'Save', kbd: { keys: ['command'], content: 'S' } }, { value: 'delete', label: 'Delete', variant: 'danger', kbd: { keys: ['command', 'shift'], content: 'D' } },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Actions </Button> </Dropdown></template>With Submenus
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'const items: DropdownEntry[] = [ { value: 'copy-link', label: 'Copy Link' }, { value: 'facebook', label: 'Facebook' }, { value: 'twitter', label: 'X / Twitter' }, { value: 'share', label: 'Other', items: [ { value: 'whatsapp', label: 'WhatsApp' }, { value: 'telegram', label: 'Telegram' }, { value: 'discord', label: 'Discord' }, { value: 'email', label: 'Email', items: [ { value: 'work', label: 'Work email' }, { value: 'personal', label: 'Personal email' }, ], }, ], },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Share </Button> </Dropdown></template>With Custom Submenu Indicator
The #submenu-indicator slot receives the item. Items the slot renders nothing for keep the default chevron.
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'import { Icon } from '@iconify/vue'const items: DropdownEntry[] = [ { value: 'copy-link', label: 'Copy Link' }, { value: 'facebook', label: 'Facebook' }, { value: 'share', label: 'More options', items: [ { value: 'whatsapp', label: 'WhatsApp' }, { value: 'telegram', label: 'Telegram' }, { value: 'email', label: 'Email', items: [ { value: 'work', label: 'Work email' }, { value: 'personal', label: 'Personal email' }, ], }, { value: 'discord', label: 'Discord' }, ], }, { value: 'other', label: 'Other (default indicator)', items: [{ value: 'sms', label: 'SMS' }], },]</script><template> <Dropdown :items="items" @action="(key) => console.log(`Selected: ${key}`)"> <Button aria-label="Menu" variant="secondary"> Share </Button> <!-- Items the slot renders nothing for keep the default chevron --> <template #submenu-indicator="{ item }"> <Icon v-if="item.value === 'share'" class="size-3.5 text-muted" icon="gravity-ui:arrow-right" /> <svg v-else-if="item.value === 'email'" class="size-3.5 text-muted" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="0 0 24 24" > <path d="M9 18l6-6-6-6" /> </svg> </template> </Dropdown></template>Custom Trigger
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Avatar, Dropdown, Label } from '@hareui/vue'import { Icon } from '@iconify/vue'// Equivalent of HeroUI's `Dropdown.Trigger` (a pressable wrapper styled by `.dropdown__trigger`)// around non-button content.const triggerClass = [ 'inline-block rounded-full outline-none transform-gpu cursor-(--cursor-interactive)', '[transition:transform_250ms_var(--ease-out-quart),background-color_150ms_var(--ease-smooth),box-shadow_150ms_var(--ease-out)] motion-reduce:transition-none', 'focus-visible:status-focused active:[transform:scale(0.97)]',].join(' ')const items: DropdownEntry[] = [ { value: 'dashboard', label: 'Dashboard' }, { value: 'profile', label: 'Profile' }, { value: 'settings', label: 'Settings', icon: 'gravity-ui:gear' }, { value: 'new-project', label: 'Create Team', icon: 'gravity-ui:persons' }, { value: 'logout', label: 'Log Out', icon: 'gravity-ui:arrow-right-from-square', variant: 'danger' },]</script><template> <Dropdown :items="items"> <button data-slot="dropdown-trigger" type="button" :class="triggerClass"> <Avatar alt="Junior Garcia" :delay-ms="600" fallback="JD" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg" /> </button> <template #content-top> <div class="px-3 pt-3 pb-1"> <div class="flex items-center gap-2"> <Avatar alt="Jane" :delay-ms="600" fallback="JD" size="sm" src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg" /> <div class="flex flex-col gap-0"> <p class="text-sm leading-5 font-medium"> Jane Doe </p> <p class="text-xs leading-none text-muted"> [email protected] </p> </div> </div> </div> </template> <template #item="{ item }"> <div v-if="item.icon" class="flex w-full items-center justify-between gap-2"> <Label>{{ item.label }}</Label> <Icon class="size-3.5" :class="item.variant === 'danger' ? 'text-danger' : 'text-muted'" :icon="item.icon" /> </div> <Label v-else> {{ item.label }} </Label> </template> </Dropdown></template>Long Press Trigger
With trigger="longPress" the menu opens on a long press, or with Alt + ArrowDown / Alt + ArrowUp, so a regular press keeps the trigger's own action.
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'const items: DropdownEntry[] = [ { value: 'new-file', label: 'New file' }, { value: 'open-file', label: 'Open file' }, { value: 'save-file', label: 'Save file' }, { value: 'delete-file', label: 'Delete file', variant: 'danger' },]</script><template> <Dropdown :items="items" trigger="longPress"> <Button aria-label="Menu" variant="secondary"> Long Press </Button> </Dropdown></template>Customization
Tailwind CSS
<script setup lang="ts">import type { DropdownEntry } from '@hareui/vue'import { Button, Dropdown } from '@hareui/vue'const items: DropdownEntry[] = [ { value: 'rename', label: 'Rename', class: 'rounded-lg data-[focused=true]:bg-default' }, { value: 'duplicate', label: 'Duplicate', class: 'rounded-lg data-[focused=true]:bg-default' }, { value: 'delete', label: 'Delete', variant: 'danger', class: 'rounded-lg data-[focused=true]:bg-danger-soft' },]</script><template> <Dropdown class="rounded-xl border border-border/80 bg-surface p-1 shadow-sm" :items="items"> <Button variant="secondary"> Actions </Button> </Dropdown></template>Global Configuration
To customize the Dropdown classes for the whole app, extend the dropdown theme in createHareUI:
app.use(createHareUI({
ui: {
dropdown: {
slots: {
popover: 'rounded-lg border border-border bg-overlay p-2',
menu: 'flex flex-col gap-1',
},
},
},
}))Styling Reference
ui key | data-slot | Element |
|---|---|---|
popover | dropdown-popover | The popover container (Reka DropdownMenuContent, which carries role="menu"); class merges here |
menu | dropdown-menu | The menu container inside the popover |
item | menu-item | Menu item; per-item classes go in item.class |
indicator | menu-item-indicator | Selection indicator (menu-item-indicator--checkmark / menu-item-indicator--dot SVG) |
submenuIndicator | submenu-indicator | Submenu chevron |
section | - | Section group (<section role="group">, labelled by its header) |
header | header | Section header, labels the group |
separator | separator | Divider between entries |
Item labels and descriptions render with the Label and Description components (data-slot="label" / data-slot="description"), and shortcuts with Kbd (variant="light").
Variants
variant="default"- Default item styling (per item,item.variant)variant="danger"- Danger item styling: label and indicators in the danger colour
Interactive States
The component supports both CSS pseudo-classes and data attributes for flexibility:
- Hover:
:hoveror[data-hovered="true"]on items - Focus:
[data-focused="true"]on the focused item,[data-focus-visible="true"]for keyboard focus only - Disabled:
[data-disabled]on items - Pressed:
:activeor[data-pressed="true"]on items - Selected:
[data-selected="true"]or[aria-checked="true"]on items - Has submenu:
[data-has-submenu="true"]on submenu triggers - Selection mode:
[data-selection-mode="single|multiple"]on the menu and its selectable items - Entering / Exiting:
[data-state="open"]/[data-state="closed"]on the popover, mapped from HeroUI's[data-entering]/[data-exiting]
API Reference
Props
| Prop | Type | Default | Description |
|---|---|---|---|
items | DropdownEntry[] | - | The menu entries: items, { type: 'section' } groups and { type: 'separator' } dividers. |
open | boolean | undefined | The controlled open state of the menu. Use with v-model:open. |
defaultOpen | boolean | - | The open state when initially rendered. Use when you do not need to control the open state. |
modelValue | DropdownValue[] | undefined | The selected values (v-model). Always an array, as sections can carry their own selection. |
defaultValue | DropdownValue[] | - | The initially selected values, when uncontrolled. |
selectionMode | "multiple" | "single" | "none" | "none" | The type of selection allowed in the menu. |
indicator | false | "checkmark" | "dot" | "checkmark" | The indicator rendered on selectable items: a checkmark, a dot, or nothing. |
trigger | "press" | "longPress" | "press" | How the menu opens: on press, or on long press (and Alt + ArrowDown / Alt + ArrowUp)
so the trigger can keep its own press action. |
disabled | boolean | - | Disables the trigger. |
side | "top" | "bottom" | "right" | "left" | "bottom" | The preferred side of the trigger to render against when open. |
align | "center" | "start" | "end" | "start" | The preferred alignment against the trigger. |
sideOffset | number | 8 | The distance in pixels from the trigger. |
ui | ComponentSlots<{ slots: { popover: string[]; menu: string[]; item: string[]; indicator: string[]; submenuIndicator: string[]; section: string; header: string; separator: string; }; variants: { variant: { default: {}; danger: { item: string; indicator: string; submenuIndicator: string; }; }; }; defaultVariants: { variant: string; }; }> | - | - |
Slots
| Slot | Props | Description |
|---|---|---|
default | any | The trigger. Merged onto its single root element (as-child). |
content-top | any | Content above the menu, inside the popover (e.g. a user card). |
item | DropdownItemSlotProps | Replaces the whole item content. |
item-leading | DropdownItemSlotProps | - |
item-label | DropdownItemSlotProps | - |
item-description | DropdownItemSlotProps | - |
item-trailing | DropdownItemSlotProps | Replaces the keyboard shortcut. |
indicator | DropdownItemSlotProps | Custom selection indicator content (rendered inside the indicator box). |
submenu-indicator | DropdownItemSlotProps | Custom submenu chevron. |
section-label | { section: DropdownSection; } | - |
Emits
| Event | Payload | Description |
|---|---|---|
update:modelValue | [value: DropdownValue[]] | - |
update:open | [value: boolean] | - |
action | [value: DropdownValue] | - |
Accessibility
Implements the WAI-ARIA Menu pattern via Reka UI's DropdownMenu primitive:
- Full keyboard navigation (arrow keys,
Home/End, typeahead usingtextValueorlabel) - Selectable items use
role="menuitemradio"(single) orrole="menuitemcheckbox"(multiple) witharia-checked - Item labels name the item; descriptions and keyboard shortcuts describe it
- Focus moves into the menu on open and returns to the trigger on close
- Disabled items are skipped
- Long press support, announced through the trigger's
aria-describedby - Submenus open with
ArrowRightand close withArrowLeft
Pointer selection closes the menu unless the selection is multiple. With the keyboard, Enter selects and closes, while Space toggles selectable items and keeps the menu open.







