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 keydata-slotElement
popoverdropdown-popoverThe popover container (Reka DropdownMenuContent, which carries role="menu"); class merges here
menudropdown-menuThe menu container inside the popover
itemmenu-itemMenu item; per-item classes go in item.class
indicatormenu-item-indicatorSelection indicator (menu-item-indicator--checkmark / menu-item-indicator--dot SVG)
submenuIndicatorsubmenu-indicatorSubmenu chevron
section-Section group (<section role="group">, labelled by its header)
headerheaderSection header, labels the group
separatorseparatorDivider 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: :hover or [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: :active or [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

PropTypeDefaultDescription
itemsDropdownEntry[]-The menu entries: items, { type: 'section' } groups and { type: 'separator' } dividers.
openbooleanundefinedThe controlled open state of the menu. Use with v-model:open.
defaultOpenboolean-The open state when initially rendered. Use when you do not need to control the open state.
modelValueDropdownValue[]undefinedThe selected values (v-model). Always an array, as sections can carry their own selection.
defaultValueDropdownValue[]-The initially selected values, when uncontrolled.
selectionMode"multiple" | "single" | "none""none"The type of selection allowed in the menu.
indicatorfalse | "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.
disabledboolean-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.
sideOffsetnumber8The distance in pixels from the trigger.
uiComponentSlots<{ 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

SlotPropsDescription
defaultanyThe trigger. Merged onto its single root element (as-child).
content-topanyContent above the menu, inside the popover (e.g. a user card).
itemDropdownItemSlotPropsReplaces the whole item content.
item-leadingDropdownItemSlotProps-
item-labelDropdownItemSlotProps-
item-descriptionDropdownItemSlotProps-
item-trailingDropdownItemSlotPropsReplaces the keyboard shortcut.
indicatorDropdownItemSlotPropsCustom selection indicator content (rendered inside the indicator box).
submenu-indicatorDropdownItemSlotPropsCustom submenu chevron.
section-label{ section: DropdownSection; }-

Emits

EventPayloadDescription
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 using textValue or label)
  • Selectable items use role="menuitemradio" (single) or role="menuitemcheckbox" (multiple) with aria-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 ArrowRight and close with ArrowLeft

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.

On this page

No Headings