Skip to content
API 风格:

Dropdown ​

A menu of commands that drops down from a trigger. The trigger can be any element; items are identified by command, which is emitted as a command event on click. Implemented after the WAI-ARIA menu pattern: arrow keys cycle, Home/End jump to the ends, Enter/Space activate, Escape closes and returns focus.

Basic usage ​

click trigger + command event

Last command: —

vue
<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Actions' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">New</LhDropdownItem>
      <LhDropdownItem command="edit">Edit</LhDropdownItem>
      <LhDropdownItem command="delete">Delete</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

<script setup lang="ts">
import { ref } from 'vue'
// The trigger label is owned by the consumer: command only carries the value,
// how it is displayed is up to the parent
const lastCommand = ref('')
const labelMap: Record<string, string> = { new: 'New', edit: 'Edit', delete: 'Delete' }
const selectedLabel = ref('')
function onCommand(command: string | number | undefined) {
  lastCommand.value = String(command)
  selectedLabel.value = labelMap[String(command)] ?? String(command)
}
</script>
vue
<script>
export default {
  data() {
    return {
      lastCommand: '',
      // The trigger label is owned by the consumer: command only carries the value,
      // how it is displayed is up to the parent
      labelMap: { new: 'New', edit: 'Edit', delete: 'Delete' },
      selectedLabel: '',
    }
  },
  methods: {
    onCommand(command) {
      this.lastCommand = String(command)
      this.selectedLabel = this.labelMap[String(command)] ?? String(command)
    },
  },
}
</script>

<template>
  <LhDropdown @command="onCommand">
    <LhButton>{{ selectedLabel || 'Actions' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="new">New</LhDropdownItem>
      <LhDropdownItem command="edit">Edit</LhDropdownItem>
      <LhDropdownItem command="delete">Delete</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Trigger modes ​

The default is click; hover and contextmenu are opt-in.

hover / contextmenu
vue
<script setup lang="ts">
import { ref } from 'vue'
const hoverLabel = ref('')
function onHoverCommand(command: string | number | undefined) {
  hoverLabel.value = String(command)
}
const contextLabel = ref('')
function onContextCommand(command: string | number | undefined) {
  contextLabel.value = String(command)
}
</script>

<template>
  <!-- Hover trigger -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Hover trigger' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Option one</LhDropdownItem>
      <LhDropdownItem command="hover-2">Option two</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Right-click trigger -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Right-click trigger' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Refresh</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Copy</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { hoverLabel: '', contextLabel: '' }
  },
  methods: {
    onHoverCommand(command) {
      this.hoverLabel = String(command)
    },
    onContextCommand(command) {
      this.contextLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Hover trigger -->
  <LhDropdown trigger="hover" @command="onHoverCommand">
    <LhButton>{{ hoverLabel || 'Hover trigger' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="hover-1">Option one</LhDropdownItem>
      <LhDropdownItem command="hover-2">Option two</LhDropdownItem>
    </template>
  </LhDropdown>
  <!-- Right-click trigger -->
  <LhDropdown trigger="contextmenu" @command="onContextCommand">
    <LhButton>{{ contextLabel || 'Right-click trigger' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="ctx-1">Refresh</LhDropdownItem>
      <LhDropdownItem command="ctx-2">Copy</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

The contextmenu trigger suppresses the browser's native context menu.

Disabled items and dividers ​

disabled / divided
vue
<script setup lang="ts">
import { ref } from 'vue'
const actionLabel = ref('')
function onActionCommand(command: string | number | undefined) {
  actionLabel.value = String(command)
}
</script>

<template>
  <!-- Actions menu with divided and disabled items -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Actions' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Export</LhDropdownItem>
      <LhDropdownItem command="import">Import</LhDropdownItem>
      <LhDropdownItem divided command="audit">Audit log</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Purge data</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { actionLabel: '' }
  },
  methods: {
    onActionCommand(command) {
      this.actionLabel = String(command)
    },
  },
}
</script>

<template>
  <!-- Actions menu with divided and disabled items -->
  <LhDropdown @command="onActionCommand">
    <LhButton>{{ actionLabel || 'Actions' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="export">Export</LhDropdownItem>
      <LhDropdownItem command="import">Import</LhDropdownItem>
      <LhDropdownItem divided command="audit">Audit log</LhDropdownItem>
      <LhDropdownItem disabled command="purge">Purge data</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

A disabled item stays focusable (aria-disabled="true") so screen readers can announce it, but arrow keys skip it and neither clicks nor Enter/Space emit command.

Placement and max height ​

placement with maxHeight
vue
<script setup lang="ts">
import { ref } from 'vue'
const itemLabel = ref('')
function onItemCommand(command: string | number | undefined) {
  itemLabel.value = `Item ${command}`
}
</script>

<template>
  <!-- Bottom-end placement with a max height -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Bottom-end' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Item {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { itemLabel: '' }
  },
  methods: {
    onItemCommand(command) {
      this.itemLabel = `Item ${command}`
    },
  },
}
</script>

<template>
  <!-- Bottom-end placement with a max height -->
  <LhDropdown placement="bottom-end" :max-height="160" @command="onItemCommand">
    <LhButton>{{ itemLabel || 'Bottom-end' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem v-for="i in 12" :key="i" :command="i">Item {{ i }}</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Controlled visibility and click behavior ​

v-model:visible controls visibility manually; hide-on-click decides whether the menu closes after a selection; popper-class adds a custom class to the popup; teleport-target selects the mount container.

Controlled visibility
vue
<script setup lang="ts">
import { ref } from 'vue'
const menuVisible = ref(false)
</script>

<template>
  <!-- Controlled visibility: the menu stays open after a click -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Collapse' : 'Expand' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Copy (menu stays open)</LhDropdownItem>
      <LhDropdownItem command="paste">Paste</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {
  data() {
    return { menuVisible: false }
  },
}
</script>

<template>
  <!-- Controlled visibility: the menu stays open after a click -->
  <LhDropdown
    v-model:visible="menuVisible"
    :hide-on-click="false"
    popper-class="demo-dropdown-popper"
  >
    <LhButton>{{ menuVisible ? 'Collapse' : 'Expand' }} ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="copy">Copy (menu stays open)</LhDropdownItem>
      <LhDropdownItem command="paste">Paste</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
Mount target
vue
<script setup lang="ts">
</script>

<template>
  <!-- Mount into a specific container -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>More ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Option</LhDropdownItem>
    </template>
  </LhDropdown>
</template>
vue
<script>
export default {}
</script>

<template>
  <!-- Mount into a specific container -->
  <LhDropdown teleport-target="#dropdown-zone">
    <LhButton>More ▾</LhButton>
    <template #dropdown>
      <LhDropdownItem command="a">Option</LhDropdownItem>
    </template>
  </LhDropdown>
</template>

Props ​

NameDescriptionTypeDefault
triggerTrigger mode"hover" | "click" | "contextmenu"'click'
placementPlacement (12 values, same as Tooltip)"top" | "top-start" | "top-end" | "bottom" | "bottom-start" | "bottom-end" | "left" | "left-start" | "left-end" | "right" | "right-start" | "right-end"'bottom-start'
visibleVisibility (v-model:visible)boolean—
disabledDisable the triggerboolean—
hideOnClickClose the menu after an item is clickedbooleantrue
teleportTargetTeleport target (selector or element)string | HTMLElement'body'
popperClassCustom class for the popperstring—
maxHeightMenu max height (numbers are px)string | number—
themeVarsComponent-level theme variables{ [x: string]: string; }—
bgPatternComponent-level background pattern, overriding the ConfigProvider configurationstring—
sxsx shorthand styles (applied to the trigger root)LhSxProps—

Events ​

NameDescriptionParameters
commandEmitted when an item is clicked(command: string | number | undefined)
clickA menu item (LhDropdownItem) is clicked(command: string | number | undefined)
visible-changeVisibility changed(value: boolean)
update:visibleVisibility changed (v-model)(value: boolean)

Slots ​

NameDescription
defaultTrigger content
dropdownMenu content (a list of LhDropdownItem)

Methods ​

MethodDescription
showOpen programmatically
hideClose programmatically
toggleToggle programmatically

Props ​

NameDescriptionTypeDefault
commandIdentifier emitted with the command eventstring | number—
disabledDisable the itembooleanfalse
dividedShow a divider above the itembooleanfalse

Accessibility ​

  • The trigger carries aria-haspopup="menu" and aria-expanded; the menu is a role="menu" and items are role="menuitem"
  • Keyboard follows the APG menu pattern: ↑/↓ cycle (skipping disabled items), Home/End jump to the ends, Enter/Space emit command and close, Escape closes and returns focus
  • Disabled items use aria-disabled rather than being removed from the focus tree; arrow keys and Enter both skip them
  • The menu flips on the main axis when it would overflow the viewport and is clamped back on the cross axis

Type Definitions ​

The TypeScript type structures exported by this component (auto-extracted from packages/components/dropdown/src/types.ts and @lh-ui/share/core/* sources; click each entry to expand; see the Props table above for attribute descriptions):

LhDropdownProps
ts
export interface LhDropdownProps {
  trigger?: LhDropdownTrigger
  placement?: LhDropdownPlacement
  visible?: boolean
  disabled?: boolean
  hideOnClick?: boolean
  teleportTarget?: string | HTMLElement
  popperClass?: string
  maxHeight?: string | number
  themeVars?: Record<string, string>
  bgPattern?: string
  sx?: LhSxProps
}
LhDropdownItemProps
ts
export interface LhDropdownItemProps {
  command?: string | number
  disabled?: boolean
  divided?: boolean
}
LhDropdownTrigger (from @lh-ui/share/core/dropdown (logic.ts))
ts
export type LhDropdownTrigger = 'hover' | 'click' | 'contextmenu'

Beauty of tradition, foundation of code