Skip to content
RTL Support: Full
Accessibility: Full
Translations: Not Needed

Dropdown

A highly customizable and extensible dropdown component that supports flat lists, nested tree navigation, search, selection states, and rich option rendering.

Basic Dropdown

The basic dropdown component provides a way to display a list of selectable options in a popup menu. It expects the trigger element to be the default slot.

Reactivity Note

options and nested children are tracked shallowly. When your data changes, replace the array reference (for example options.value = [...nextOptions]) instead of mutating in place so the menu state, search index, and keyboard navigation refresh correctly.

Here's a simple dropdown with click trigger and basic options:

vue
<template>
  <div class="flex gap-4 items-center">
    <HLDropdown
      id="basic-dropdown"
      trigger="click"
      placement="bottom"
      :options="basicOptions"
      :show-search="false"
      width="200"
      @select="handleSelect"
    >
      <HLButton size="sm">Basic Dropdown ({{ basicOptions.length }})</HLButton>
    </HLDropdown>
    <HLButton size="sm" variant="primary" color="blue" @click="addBasicOption">Add option</HLButton>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
const basicOptions = ref([...simpleOptions])

// Options are tracked shallowly: REPLACE the array reference so the menu,
// search index, and keyboard navigation pick up the change. Pushing into
// basicOptions.value in place would NOT refresh the menu.
const addBasicOption = () => {
  const next = basicOptions.value.length + 1
  basicOptions.value = [...basicOptions.value, { key: `extra-${next}`, label: `New Option ${next}` }]
}

const handleSelect = (key, option) => {
  console.log('selected', key, option)
}
</script>
ts
// Define your options - each must have a unique key and label
const simpleOptions = [
  {
    key: 'edit',
    label: 'Edit Document',
  },
  {
    key: 'share',
    label: 'Share with Team',
  },
  {
    key: 'download',
    label: 'Download as PDF',
  },
  {
    key: 'duplicate',
    label: 'Make a Copy',
  },
  {
    key: 'archive',
    label: 'Archive Document',
  },
  {
    key: 'delete',
    label: 'Delete Document',
  },
]

1. Trigger Types

Choose how users activate the dropdown:

vue
<template>
  <div class="flex gap-4">
    <!-- Click Trigger (Default) -->
    <HLDropdown trigger="click" :options="simpleOptions" :show-search="false">
      <HLButton>Click Me</HLButton>
    </HLDropdown>

    <!-- Hover Trigger -->
    <HLDropdown trigger="hover" :options="simpleOptions" :show-search="false">
      <HLButton>Hover Me</HLButton>
    </HLDropdown>
  </div>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
// Define your options - each must have a unique key and label
const simpleOptions = [
  {
    key: 'edit',
    label: 'Edit Document',
  },
  {
    key: 'share',
    label: 'Share with Team',
  },
  {
    key: 'download',
    label: 'Download as PDF',
  },
  {
    key: 'duplicate',
    label: 'Make a Copy',
  },
  {
    key: 'archive',
    label: 'Archive Document',
  },
  {
    key: 'delete',
    label: 'Delete Document',
  },
]

Placement Options

Position your dropdown relative to the trigger element in any of the following directions: top-start, top, top-end, right-start, right, right-end, bottom-start, bottom, bottom-end, left-start, left, left-end

vue
<template>
  <div class="flex gap-4">
    <!-- Different placement examples -->
    <HLDropdown id="placement-demo-1" placement="top-start" :options="simpleOptions" :show-search="false" width="200">
      <HLButton size="sm">Opens Top Start</HLButton>
    </HLDropdown>

    <HLDropdown id="placement-demo-2" placement="bottom-end" :options="simpleOptions" :show-search="false" width="200">
      <HLButton size="sm">Opens Bottom End</HLButton>
    </HLDropdown>

    <HLDropdown id="placement-demo-3" placement="right" :options="simpleOptions" :show-search="false" width="200">
      <HLButton size="sm">Opens Right</HLButton>
    </HLDropdown>
  </div>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
// Define your options - each must have a unique key and label
const simpleOptions = [
  {
    key: 'edit',
    label: 'Edit Document',
  },
  {
    key: 'share',
    label: 'Share with Team',
  },
  {
    key: 'download',
    label: 'Download as PDF',
  },
  {
    key: 'duplicate',
    label: 'Make a Copy',
  },
  {
    key: 'archive',
    label: 'Archive Document',
  },
  {
    key: 'delete',
    label: 'Delete Document',
  },
]

Nested Options

The dropdown component supports nested options, which can be displayed in either a hierarchical cascade or a tree structure.

1. Cascading Dropdown

A cascading dropdown displays options in a linear sequence, where each option's children are displayed as a new dropdown.

vue
<template>
  <HLDropdown id="nested-dropdown" trigger="click" placement="bottom" :options="nestedOptions" width="200">
    <HLButton size="sm">Nested Dropdown</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// nestedOptions defined below (Dropdown Options tab)
</script>
ts
const nestedOptions = [
  {
    key: 'produce',
    label: 'Fresh Produce',
    children: [
      {
        key: 'fruits',
        label: 'Fruits & Berries',
        children: [
          {
            key: 'tropical',
            label: 'Tropical Fruits',
            children: [
              { key: 'mango', label: 'Mango', description: 'Sweet and juicy' },
              { key: 'pineapple', label: 'Pineapple', description: 'Tangy and tropical' },
              { key: 'papaya', label: 'Papaya', description: 'Soft and sweet' },
            ],
          },
          {
            key: 'berries',
            label: 'Fresh Berries',
            children: [
              { key: 'strawberry', label: 'Strawberry', description: 'Red and fragrant' },
              { key: 'blueberry', label: 'Blueberry', description: 'Small and antioxidant-rich' },
              { key: 'raspberry', label: 'Raspberry', description: 'Tart and delicate' },
            ],
          },
        ],
      },
      {
        key: 'vegetables',
        label: 'Vegetables',
        children: [
          {
            key: 'leafy',
            label: 'Leafy Greens',
            children: [
              { key: 'spinach', label: 'Spinach', description: 'Dark and nutritious' },
              { key: 'kale', label: 'Kale', description: 'Crispy and healthy' },
              { key: 'lettuce', label: 'Lettuce', description: 'Fresh and crisp' },
            ],
          },
          {
            key: 'root',
            label: 'Root Vegetables',
            children: [
              { key: 'carrot', label: 'Carrot', description: 'Orange and crunchy' },
              { key: 'potato', label: 'Potato', description: 'Starchy and versatile' },
              { key: 'beet', label: 'Beet', description: 'Deep red and earthy' },
            ],
          },
        ],
      },
    ],
  },
  {
    key: 'dairy',
    label: 'Dairy & Eggs',
    children: [
      {
        key: 'milk_products',
        label: 'Milk Products',
        children: [
          {
            key: 'fresh_milk',
            label: 'Fresh Milk',
            children: [
              { key: 'whole_milk', label: 'Whole Milk', description: 'Full fat and creamy' },
              { key: 'reduced_fat', label: '2% Milk', description: 'Reduced fat option' },
              { key: 'skim_milk', label: 'Skim Milk', description: 'Fat-free option' },
            ],
          },
          {
            key: 'yogurt',
            label: 'Yogurt',
            children: [
              { key: 'greek', label: 'Greek Yogurt', description: 'Thick and protein-rich' },
              { key: 'regular', label: 'Regular Yogurt', description: 'Smooth and creamy' },
              { key: 'probiotic', label: 'Probiotic Yogurt', description: 'With live cultures' },
            ],
          },
        ],
      },
      {
        key: 'cheese',
        label: 'Cheese',
        children: [
          {
            key: 'hard_cheese',
            label: 'Hard Cheese',
            children: [
              { key: 'cheddar', label: 'Cheddar', description: 'Sharp and aged' },
              { key: 'parmesan', label: 'Parmesan', description: 'Granular and salty' },
              { key: 'gouda', label: 'Gouda', description: 'Rich and smooth' },
            ],
          },
          {
            key: 'soft_cheese',
            label: 'Soft Cheese',
            children: [
              { key: 'brie', label: 'Brie', description: 'Creamy and mild' },
              { key: 'mozzarella', label: 'Mozzarella', description: 'Fresh and milky' },
              { key: 'camembert', label: 'Camembert', description: 'Rich and buttery' },
            ],
          },
        ],
      },
    ],
  },
]

2. Popover Props With Nested Options

Use popoverProps to pass HLPopover overrides to the dropdown menu. In cascade mode, the same overrides are applied to the root menu popover and each nested submenu popover.

vue
<template>
  <HLDropdown
    id="nested-popover-props-dropdown"
    trigger="hover"
    placement="bottom"
    :options="nestedOptions"
    :width="240"
    :popover-props="{ displayDirective: 'show' }">
    <HLButton size="sm">Nested Dropdown With Popover Props</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// nestedOptions defined below (Dropdown Options tab)
</script>
ts
const nestedOptions = [
  {
    key: 'produce',
    label: 'Fresh Produce',
    children: [
      {
        key: 'fruits',
        label: 'Fruits & Berries',
        children: [
          { key: 'mango', label: 'Mango', description: 'Sweet and juicy' },
          { key: 'pineapple', label: 'Pineapple', description: 'Tangy and tropical' },
          { key: 'papaya', label: 'Papaya', description: 'Soft and sweet' },
        ],
      },
      {
        key: 'vegetables',
        label: 'Vegetables',
        children: [
          { key: 'spinach', label: 'Spinach', description: 'Dark and nutritious' },
          { key: 'kale', label: 'Kale', description: 'Crispy and healthy' },
          { key: 'lettuce', label: 'Lettuce', description: 'Fresh and crisp' },
        ],
      },
    ],
  },
  {
    key: 'dairy',
    label: 'Dairy & Eggs',
    children: [
      { key: 'whole_milk', label: 'Whole Milk', description: 'Full fat and creamy' },
      { key: 'cheddar', label: 'Cheddar', description: 'Sharp and aged' },
      { key: 'greek', label: 'Greek Yogurt', description: 'Thick and protein-rich' },
    ],
  },
]

3. Dropdown Tree

The same options can be displayed in a nested tree structure, which enables hierarchical navigation:

vue
<template>
  <HLDropdown id="tree-dropdown" trigger="click" placement="bottom" :options="nestedOptions" tree-mode show-search :width="280">
    <HLButton size="sm">Dropdown Tree</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// nestedOptions defined below (Dropdown Tree Options tab)
</script>
ts
const nestedOptions = [
  {
    key: 'produce',
    label: 'Fresh Produce',
    children: [
      {
        key: 'fruits',
        label: 'Fruits & Berries',
        children: [
          {
            key: 'tropical',
            label: 'Tropical Fruits',
            children: [
              { key: 'mango', label: 'Mango', description: 'Sweet and juicy' },
              { key: 'pineapple', label: 'Pineapple', description: 'Tangy and tropical' },
              { key: 'papaya', label: 'Papaya', description: 'Soft and sweet' },
            ],
          },
          {
            key: 'berries',
            label: 'Fresh Berries',
            children: [
              { key: 'strawberry', label: 'Strawberry', description: 'Red and fragrant' },
              { key: 'blueberry', label: 'Blueberry', description: 'Small and antioxidant-rich' },
              { key: 'raspberry', label: 'Raspberry', description: 'Tart and delicate' },
            ],
          },
        ],
      },
      {
        key: 'vegetables',
        label: 'Vegetables',
        children: [
          {
            key: 'leafy',
            label: 'Leafy Greens',
            children: [
              { key: 'spinach', label: 'Spinach', description: 'Dark and nutritious' },
              { key: 'kale', label: 'Kale', description: 'Crispy and healthy' },
              { key: 'lettuce', label: 'Lettuce', description: 'Fresh and crisp' },
            ],
          },
          {
            key: 'root',
            label: 'Root Vegetables',
            children: [
              { key: 'carrot', label: 'Carrot', description: 'Orange and crunchy' },
              { key: 'potato', label: 'Potato', description: 'Starchy and versatile' },
              { key: 'beet', label: 'Beet', description: 'Deep red and earthy' },
            ],
          },
        ],
      },
    ],
  },
  {
    key: 'dairy',
    label: 'Dairy & Eggs',
    children: [
      {
        key: 'milk_products',
        label: 'Milk Products',
        children: [
          {
            key: 'fresh_milk',
            label: 'Fresh Milk',
            children: [
              { key: 'whole_milk', label: 'Whole Milk', description: 'Full fat and creamy' },
              { key: 'reduced_fat', label: '2% Milk', description: 'Reduced fat option' },
              { key: 'skim_milk', label: 'Skim Milk', description: 'Fat-free option' },
            ],
          },
          {
            key: 'yogurt',
            label: 'Yogurt',
            children: [
              { key: 'greek', label: 'Greek Yogurt', description: 'Thick and protein-rich' },
              { key: 'regular', label: 'Regular Yogurt', description: 'Smooth and creamy' },
              { key: 'probiotic', label: 'Probiotic Yogurt', description: 'With live cultures' },
            ],
          },
        ],
      },
      {
        key: 'cheese',
        label: 'Cheese',
        children: [
          {
            key: 'hard_cheese',
            label: 'Hard Cheese',
            children: [
              { key: 'cheddar', label: 'Cheddar', description: 'Sharp and aged' },
              { key: 'parmesan', label: 'Parmesan', description: 'Granular and salty' },
              { key: 'gouda', label: 'Gouda', description: 'Rich and smooth' },
            ],
          },
          {
            key: 'soft_cheese',
            label: 'Soft Cheese',
            children: [
              { key: 'brie', label: 'Brie', description: 'Creamy and mild' },
              { key: 'mozzarella', label: 'Mozzarella', description: 'Fresh and milky' },
              { key: 'camembert', label: 'Camembert', description: 'Rich and buttery' },
            ],
          },
        ],
      },
    ],
  },
]

The dropdown component includes a built-in search functionality that is enabled by default (show-search prop defaults to true). The search feature:

  • Filters through leaf nodes (options without children) in both flat and nested structures
  • Matches case-insensitive text against the option's label property
  • Displays the full path for nested options (e.g., "Parent / Child")
  • Can be disabled by setting show-search to false
vue
<template>
  <HLDropdown id="basic-dropdown" trigger="click" placement="bottom" :options="longOptions" width="200" show-search>
    <HLButton size="sm">Basic Dropdown with Search</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// longOptions defined below (Dropdown Options tab)
</script>
ts
const longOptions = [
  {
    key: 'option1',
    label: 'Placeholder',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'option2',
    label: 'Jot something here',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'option3',
    label: 'Get the job done',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'header',
    label: 'Actions',
    type: 'header',
  },
  {
    key: 'settings',
    label: 'Settings',
    children: [
      { key: 'notifications', label: 'Notifications' },
      { key: 'privacy', label: 'Privacy & Security' },
      { key: 'appearance', label: 'Appearance' },
      { key: 'language', label: 'Language & Region' },
    ],
  },
]

Search Input Options

Customize the built-in search with searchPlaceholder (the input's placeholder text) and clearableInSearch (adds a clear button to reset the query). To drive the query from your own state, bind searchValue with v-model:searchValue (or pair :searchValue with @update:searchValue) — useful to prefill, clear, or sync the query elsewhere in your UI.

vue
<template>
  <HLDropdown
    id="search-options-dropdown"
    trigger="click"
    placement="bottom"
    :options="longOptions"
    show-search
    search-placeholder="Search actions…"
    clearable-in-search
    v-model:searchValue="controlledSearch"
    :width="240"
  >
    <HLButton size="sm">Search query: {{ controlledSearch || '—' }}</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
  import { ref } from 'vue'
  import { HLDropdown, HLButton } from '@platform-ui/highrise'

  const controlledSearch = ref('')
  // longOptions defined below (Dropdown Options tab)
</script>
ts
const longOptions = [
  {
    key: 'option1',
    label: 'Placeholder',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'option2',
    label: 'Jot something here',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'option3',
    label: 'Get the job done',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'header',
    label: 'Actions',
    type: 'header',
  },
  {
    key: 'settings',
    label: 'Settings',
    children: [
      { key: 'notifications', label: 'Notifications' },
      { key: 'privacy', label: 'Privacy & Security' },
      { key: 'appearance', label: 'Appearance' },
      { key: 'language', label: 'Language & Region' },
    ],
  },
]

Search Highlight

The showSearchHighlight prop enables highlighting of search matches in option labels. When enabled, matching text in option labels will be visually highlighted.

vue
<template>
  <HLDropdown
    id="search-highlight-dropdown"
    trigger="click"
    placement="bottom"
    :options="options"
    show-search
    show-search-highlight
    width="200"
  >
    <HLButton size="sm">Search with Highlight</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// options defined below (Dropdown Options tab)
</script>
ts
const options = [
  { key: 'apple', label: 'Apple' },
  { key: 'apricot', label: 'Apricot' },
  { key: 'avocado', label: 'Avocado' },
  { key: 'banana', label: 'Banana' },
  { key: 'blueberry', label: 'Blueberry' },
]

Custom Search (Remote / Async)

The built-in search filters the options you already have on the client (see Basic search). When the data lives on a server — too large to ship up front, or changing constantly — use the @search event to fetch results on demand instead. This is the main reason to reach for custom search.

How it works:

  • Keep show-search enabled so the input renders, and listen to @search — it fires with the query string as the user types.
  • In the handler, call your API and assign the response to the array bound to :options. When you handle @search, the dropdown does no filtering of its own — it renders exactly what options holds.
  • Debounce the handler so you only call the API after the user pauses typing, rather than on every keystroke.
  • Use the loader slot to show a spinner while the request is in flight, and the empty slot for the "no results" / "type to search" state.

Open the menu and type a name or email (e.g. ava, chen ). Each search waits 300ms after you stop typing, then shows a spinner for ~500ms while the mock "API" responds:

vue
<template>
  <HLDropdown
    id="custom-search-dropdown"
    trigger="click"
    placement="bottom"
    :options="remoteOptions"
    show-search
    search-placeholder="Search users…"
    :width="300"
    @search="handleRemoteSearch"
  >
    <!-- Spinner while the request is in flight -->
    <template #loader>
      <div v-if="remoteLoading" class="py-3 text-center">
        <HLSpin size="sm" />
      </div>
    </template>
    <!-- Empty state: nudge before searching, "no results" after -->
    <template #empty>
      <div class="py-3 text-center text-sm text-gray-400">
        {{ hasSearched ? 'No users found' : 'Type to search users' }}
      </div>
    </template>
    <HLButton size="sm">Search Users</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { HLDropdown, HLButton, HLSpin } from '@platform-ui/highrise'

const remoteOptions = ref([])
const remoteLoading = ref(false)
const hasSearched = ref(false)

// Replace this with your real API call
const fetchUsers = query =>
  fetch(`/api/users?q=${encodeURIComponent(query)}`).then(res => res.json())

let timeout
const handleRemoteSearch = value => {
  clearTimeout(timeout)

  // Reset when the query is cleared
  if (!value) {
    remoteOptions.value = []
    hasSearched.value = false
    remoteLoading.value = false
    return
  }

  remoteLoading.value = true
  hasSearched.value = true

  // Debounce: only hit the API after the user pauses typing
  timeout = setTimeout(async () => {
    remoteOptions.value = await fetchUsers(value)
    remoteLoading.value = false
  }, 300)
}
</script>
ts
// The demo above swaps the fetch() for this in-memory stand-in so it runs without a backend
const USERS = [
  { key: 'u1', label: 'Ava Thompson', description: '[email protected]' },
  { key: 'u2', label: 'Liam Rodriguez', description: '[email protected]' },
  { key: 'u3', label: 'Noah Patel', description: '[email protected]' },
  { key: 'u4', label: 'Emma Chen', description: '[email protected]' },
  { key: 'u5', label: 'Olivia Martin', description: '[email protected]' },
  { key: 'u6', label: 'Sophia Nguyen', description: '[email protected]' },
]

// Returns matches after a 500ms delay to mimic network latency
const fetchUsers = query =>
  new Promise(resolve => {
    setTimeout(() => {
      const q = query.toLowerCase()
      resolve(USERS.filter(u => u.label.toLowerCase().includes(q) || u.description.toLowerCase().includes(q)))
    }, 500)
  })

Option Types

The dropdown component supports various option types, each designed for specific use cases. Below is a concise overview of each type with examples:

  1. Default Text Option - Simple text options with a key and a label.

    ts
    { key: 'default', label: 'Default Option' }
  2. Header - Used to group options together.

    ts
    { key: 'header1', label: 'Group 1', type: 'header' }
  3. Divider - Used to separate options.

    ts
    { key: 'divider1', type: 'divider' }
  4. Avatar - Displays an image.

    ts
    { key: 'avatar1', label: 'User Profile', type: 'avatar', src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=John' }
  5. Icon - Displays an icon. You can also set iconPlacement to place the icon on either side of the label. Note that if you have children, the icon will be placed on the left side by default to accomodate the chevron icon for the children.

    ts
    { key: 'icon1', label: 'Verified Account', type: 'icon', icon: CheckVerified01Icon, iconPlacement: 'left' }
  6. Description with Icon - Displays a description with an icon

    ts
    { key: 'desc1', label: 'Share Post', description: 'Share to social media channels', descriptionIcon: CheckVerified01Icon }
  7. Info Text - Displays additional text to the right of the label

    ts
    { key: 'info1', label: 'Messages', infoText: '5 unread' }
  8. Title Right Slot - Displays custom content on the right side

    ts
    { key: 'slot1', label: 'Performance', titleRightSlot: () => h(HLTag, { size: 'xs', round: true, color: 'success' }, { default: () => '↑ 10%' }) }
  9. Disabled - Disables an option

    ts
    { key: 'disabled1', label: 'Unavailable Feature', disabled: true }
  10. Render - Fully custom option content via a render function, for layouts the other types don't cover (rich cards, multi-line content, embedded components). The function returns a VNode built with h(). Note that type: 'render' options do not emit @select — add your own click handlers inside the rendered content to react to interaction.

    ts
    {
      key: 'team-member',
      label: 'Team Member',
      type: 'render',
      render: () =>
        h('div', { class: 'p-2 flex items-center justify-between' }, [
          h('div', { class: 'flex items-center gap-3' }, [
            h('img', { src: '/avatar.png', class: 'w-8 h-8 rounded-full', alt: 'Sarah Wilson' }),
            h('div', { class: 'flex flex-col' }, [
              h('div', { class: 'text-sm font-medium' }, 'Sarah Wilson'),
              h('div', { class: 'text-xs text-gray-500 flex items-center gap-1' }, [
                h('div', { class: 'w-1.5 h-1.5 rounded-full bg-success-500' }),
                'Online',
              ]),
            ]),
          ]),
          h(HLTag, { size: 'xs', color: 'warning' }, { default: () => 'Lead' }),
        ]),
    }
vue
<template>
  <HLDropdown :options="demoOptions" showSearch treeMode showSelectedMark :closeOnSelect="false" @select="handleDemoSelect">
    <HLButton>{{ demoSelectedValue }}</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'
import { ref } from 'vue'

// demoOptions defined below (Dropdown Options tab)

const demoSelectedValue = ref('Select an Option')
const handleDemoSelect = (key, option) => {
  if (option && option.label) {
    demoSelectedValue.value = option.label
  }
}
</script>
ts
import { h } from 'vue'
import { HLTag } from '@platform-ui/highrise'
import { CheckVerified01Icon } from '@gohighlevel/ghl-icons/24/outline'

const demoOptions = [
  // Default option
  {
    key: 'default',
    label: 'Default Option',
  },

  // Header option
  {
    key: 'header1',
    label: 'Group 1',
    type: 'header',
  },

  // Divider
  {
    key: 'divider1',
    type: 'divider',
  },

  // Avatar option
  {
    key: 'avatar1',
    label: 'User Profile',
    type: 'avatar',
    src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=John',
  },

  // Icon option with left placement
  {
    key: 'icon1',
    label: 'Verified Account',
    type: 'icon',
    icon: CheckVerified01Icon,
    iconPlacement: 'left',
  },

  // Option with description and icon
  {
    key: 'desc1',
    label: 'Share Post',
    description: 'Share to social media channels',
    descriptionIcon: CheckVerified01Icon,
  },

  // Option with info text
  {
    key: 'info1',
    label: 'Messages',
    infoText: '5 unread',
  },

  // Option with title right slot
  {
    key: 'slot1',
    label: 'Performance',
    titleRightSlot: () => h(HLTag, { size: 'xs', round: true, color: 'success' }, { default: () => '↑ 10%' }),
  },

  // Disabled option
  {
    key: 'disabled1',
    label: 'Unavailable Feature',
    disabled: true,
  },

  // Render option (fully custom content)
  {
    key: 'render1',
    label: 'Custom Render',
    type: 'render',
    render: () =>
      h('div', { class: 'p-2 flex items-center justify-between' }, [
        h('div', { class: 'flex items-center gap-3' }, [
          h('img', {
            src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=Sarah',
            class: 'w-8 h-8 rounded-full',
            alt: 'Sarah Wilson',
          }),
          h('div', { class: 'flex flex-col' }, [
            h('div', { class: 'text-sm font-medium' }, 'Sarah Wilson'),
            h('div', { class: 'text-xs text-gray-500 flex items-center gap-1' }, [
              h('div', { class: 'w-1.5 h-1.5 rounded-full bg-success-500' }),
              'Online',
            ]),
          ]),
        ]),
        h(HLTag, { size: 'xs', round: true, color: 'warning' }, { default: () => 'Lead' }),
      ]),
  },

  // Nested options (children)
  {
    key: 'parent1',
    label: 'Settings',
    children: [
      {
        key: 'child1',
        label: 'General',
      },
      {
        key: 'child2',
        label: 'Security',
      },
    ],
  },
]

Use the header and footer slots to add fixed content above and below the option list — for example a title or an action button. The empty slot replaces the default "no results" content shown when a search matches nothing.

vue
<template>
  <HLDropdown id="slots-dropdown" trigger="click" placement="bottom" :options="simpleOptions" show-search :width="240">
    <template #header>
      <div class="px-3 py-2 text-xs font-semibold text-gray-500">Document actions</div>
    </template>
    <template #footer>
      <div class="px-3 py-2 border-t border-gray-100">
        <HLButton size="xs" variant="text" color="blue">Manage all</HLButton>
      </div>
    </template>
    <template #empty>
      <div class="px-3 py-4 text-center text-sm text-gray-400">No matching actions</div>
    </template>
    <HLButton size="sm">Header, Footer &amp; Empty</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Custom Option Renderer

The option-renderer slot overrides how every option row is rendered, receiving each option as a slot prop. Use it when you want one consistent custom layout across all options — unlike the per-option render type, which customizes individual options.

vue
<template>
  <HLDropdown id="option-renderer-dropdown" trigger="click" placement="bottom" :options="simpleOptions" :show-search="false" :width="240">
    <template #option-renderer="{ option }">
      <div class="flex items-center justify-between px-3 py-2">
        <span class="text-sm">{{ option.label }}</span>
        <span class="text-xs text-gray-400">{{ option.key }}</span>
      </div>
    </template>
    <HLButton size="sm">Custom Option Renderer</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Width Control

Adapt the dropdown width to your content:

vue
<template>
  <div class="flex gap-4">
    <!-- Fixed width -->
    <HLDropdown id="width-demo-1" :width="200" :options="simpleOptions" :show-search="false">
      <HLButton size="sm">Fixed 200px Width</HLButton>
    </HLDropdown>

    <!-- Auto width (matches trigger width) -->
    <HLDropdown id="width-demo-2" width="auto" :options="simpleOptions" :show-search="false">
      <HLButton size="sm" class="w-[200px]">Auto Width</HLButton>
    </HLDropdown>
  </div>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
// Define your options - each must have a unique key and label
const simpleOptions = [
  {
    key: 'edit',
    label: 'Edit Document',
  },
  {
    key: 'share',
    label: 'Share with Team',
  },
  {
    key: 'download',
    label: 'Download as PDF',
  },
  {
    key: 'duplicate',
    label: 'Make a Copy',
  },
  {
    key: 'archive',
    label: 'Archive Document',
  },
  {
    key: 'delete',
    label: 'Delete Document',
  },
]

Height Limitation

The maxHeight prop can be used to limit the height of the dropdown menu, making it scrollable when content exceeds the maximum height.

vue
<template>
  <HLDropdown
    id="tree-dropdown"
    trigger="click"
    placement="bottom"
    :options="longOptions"
    tree-mode
    show-search
    max-height="200px"
    :width="280"
  >
    <HLButton size="sm">Dropdown Tree with Max Height</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// longOptions defined below (Dropdown Options tab)
</script>
ts
const longOptions = [
  {
    key: 'option1',
    label: 'Placeholder',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'option2',
    label: 'Jot something here',
    description: 'Send out this post to the selected social channels/accounts.',
  },
  {
    key: 'header',
    label: 'Actions',
    type: 'header',
  },
  {
    key: 'settings',
    label: 'Settings',
    children: [
      { key: 'notifications', label: 'Notifications' },
      { key: 'privacy', label: 'Privacy & Security' },
      { key: 'appearance', label: 'Appearance' },
      { key: 'language', label: 'Language & Region' },
      { key: 'accessibility', label: 'Accessibility' },
      { key: 'updates', label: 'Updates & Downloads' },
      { key: 'storage', label: 'Storage & Backup' },
      { key: 'help', label: 'Help & Support' },
    ],
  },
]

In cascade mode, you can specify a unique maxHeight for each submenu using the childrenMaxHeight property on parent options. This allows fine-grained control over scrolling behavior in nested menus.

  • childrenMaxHeight is set on parent options that have children
  • Each submenu can have its own unique max height
  • If childrenMaxHeight is not specified, the submenu falls back to the parent dropdown's maxHeight prop
  • This is particularly useful for deeply nested menus where different levels need different scroll heights
vue
<template>
  <HLDropdown
    id="cascade-max-height"
    trigger="hover"
    placement="bottom-start"
    :options="cascadeMaxHeightOptions"
    max-height="300px"
    :show-search="false"
    width="200"
  >
    <HLButton size="sm">Cascade with Custom Heights</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// cascadeMaxHeightOptions defined below (Dropdown Options tab)
</script>
ts
const cascadeMaxHeightOptions = [
  {
    key: 'parent1',
    label: 'Parent 1 - Scrollable Submenu',
    childrenMaxHeight: '200px', // Custom max height for this submenu
    children: Array.from({ length: 20 }, (_, i) => ({
      key: `parent1-child-${i + 1}`,
      label: `Child ${i + 1}`,
    })),
  },
  {
    key: 'parent2',
    label: 'Parent 2 - Multi-Level Nesting',
    childrenMaxHeight: '150px', // First level submenu max height
    children: [
      {
        key: 'parent2-child1',
        label: 'Level 2 - Scrollable',
        childrenMaxHeight: '200px', // Second level submenu max height
        children: Array.from({ length: 25 }, (_, i) => ({
          key: `parent2-child1-grandchild-${i + 1}`,
          label: `Grandchild ${i + 1}`,
        })),
      },
      {
        key: 'parent2-child2',
        label: 'Level 2 - Different Height',
        childrenMaxHeight: '120px', // Different max height for this submenu
        children: Array.from({ length: 15 }, (_, i) => ({
          key: `parent2-child2-grandchild-${i + 1}`,
          label: `Grandchild ${i + 1}`,
        })),
      },
    ],
  },
]

Infinite Scroll

The dropdown component supports infinite scrolling through the @scroll event. This is useful for loading large datasets progressively as the user scrolls. The scroll event is fired when maxHeight is set, making the dropdown scrollable.

vue
<template>
  <HLDropdown 
    :options="infiniteScrollOptions" 
    @scroll="handleInfiniteScroll" 
    max-height="300px"
    :show-search="false"
    :close-on-select="false"
  >
    <template #loader>
      <div v-if="infiniteLoading || hasMore" style="padding: 8px; text-align: center;">
        <HLSpin v-if="infiniteLoading" size="sm" />
      </div>
    </template>
    <HLButton size="sm">Infinite Scroll ({{ infiniteScrollOptions.length }} items)</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton, HLSpin } from '@platform-ui/highrise'

// infinite-scroll state and handlers defined below (Script tab)
</script>
ts
import { ref, onMounted } from 'vue'

const infiniteScrollOptions = ref([])
const infiniteLoading = ref(false)
const infinitePage = ref(1)
const hasMore = ref(true)
const itemsPerPage = 20

const generateInfiniteItems = (start, count) => {
  return Array.from({ length: count }, (_, index) => ({
    key: `item-${start + index}`,
    label: `Item ${start + index}`,
  }))
}

const loadMoreInfiniteItems = async () => {
  if (infiniteLoading.value || !hasMore.value) return
  
  infiniteLoading.value = true
  try {
    await new Promise(resolve => setTimeout(resolve, 800))
    const newItems = generateInfiniteItems((infinitePage.value - 1) * itemsPerPage + 1, itemsPerPage)
    infiniteScrollOptions.value = [...infiniteScrollOptions.value, ...newItems]
    infinitePage.value += 1
    hasMore.value = infinitePage.value < 6 // Limit to 5 pages (100 items)
  } finally {
    infiniteLoading.value = false
  }
}

onMounted(() => {
  loadMoreInfiniteItems()
})

const handleInfiniteScroll = async (event) => {
  const { scrollTop, scrollHeight, clientHeight } = event.target
  if (scrollHeight - scrollTop - clientHeight < 50) {
    await loadMoreInfiniteItems()
  }
}
  • The @scroll event fires when the dropdown is scrolled (requires maxHeight to be set)
  • Use the loader slot to display a loading indicator while fetching more items
  • The scroll event provides access to the scroll container, allowing you to detect when the user reaches the bottom
  • Perfect for implementing pagination or async loading of large option lists

Multiple Selection

The dropdown component supports multiple selection mode, which allows users to select multiple options. You can also control whether the dropdown should close after selection by setting the closeOnSelect prop to false. The showSelectedMark prop can be used to show a checkmark next to the selected option.

vue
<template>
  <HLDropdown
    id="multiple-dropdown"
    trigger="click"
    placement="bottom"
    :options="options"
    multiple
    :closeOnSelect="false"
    show-selected-mark
    :width="280"
  >
    <HLButton size="sm">Multiple Selection</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// options defined below (Dropdown Options tab)
</script>
ts
const options = [
  {
    key: 'fruits',
    label: 'Fruits',
    children: [
      { key: 'apple', label: 'Apple' },
      { key: 'banana', label: 'Banana' },
      { key: 'orange', label: 'Orange' },
    ],
  },
  {
    key: 'vegetables',
    label: 'Vegetables',
    children: [
      { key: 'carrot', label: 'Carrot' },
      { key: 'broccoli', label: 'Broccoli' },
      { key: 'potato', label: 'Potato' },
    ],
  },
]

Checkbox Selection

Set showCheckbox to render a checkbox before each leaf option. It is intended for multiple selection with :closeOnSelect="false", so users can tick several options without the menu closing.

vue
<template>
  <HLDropdown
    id="checkbox-dropdown"
    trigger="click"
    placement="bottom"
    :options="simpleOptions"
    multiple
    show-checkbox
    :close-on-select="false"
    :show-search="false"
    :width="240"
  >
    <HLButton size="sm">Checkbox Selection</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Disabled Options

You can disable individual options by setting the disabled key to true on the dropdown option. Disabled options cannot be selected and do not emit the select event when clicked and display a not-allowed cursor on hover.

vue
<template>
  <HLDropdown id="disabled-dropdown" trigger="click" placement="bottom" :options="disabledOptions" :width="280">
    <HLButton size="sm">Disabled Options</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// disabledOptions defined below (Dropdown Options tab)
</script>
ts
const disabledOptions = [
  {
    key: 'option1',
    label: 'Enabled Option',
    description: 'This option is selectable',
  },
  {
    key: 'option2',
    label: 'Disabled Option',
    description: 'This option cannot be selected',
    disabled: true,
  },
  {
    key: 'group',
    label: 'Mixed Group',
    children: [
      { key: 'enabled', label: 'Enabled Child' },
      { key: 'disabled-child', label: 'Disabled Child', disabled: true },
    ],
  },
]

Disable Selection Highlight

Suppress the selected-option highlight by setting the highlightSelection prop to false.

vue
<template>
  <HLDropdown
    id="hidden-selection-highlight-dropdown"
    trigger="click"
    placement="bottom"
    :options="simpleOptions"
    :highlightSelection="false"
  >
    <HLButton size="sm">Disable Selection Highlight</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Disabled Dropdown

You can also disable the entire dropdown by setting the disabled prop on the dropdown component.

WARNING

A disabled dropdown cannot be opened, but the trigger itself remains active. To disable the trigger, it must be explicitly set as disabled.

vue
<template>
  <HLDropdown id="disabled-entire-dropdown" trigger="click" placement="bottom" :options="simpleOptions" disabled :width="200">
    <HLButton size="sm">Disabled Dropdown</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Hide Arrow

The menu shows a small arrow pointing at the trigger by default. Set :show-arrow="false" to remove it.

vue
<template>
  <HLDropdown id="no-arrow-dropdown" trigger="click" placement="bottom" :options="simpleOptions" :show-arrow="false" :show-search="false" :width="200">
    <HLButton size="sm">No Arrow</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Reset Tree on Close

In tree-mode, the menu remembers how deep the user navigated. Set reset-tree-on-change so the tree returns to its top level each time the dropdown reopens.

vue
<template>
  <HLDropdown id="reset-tree-dropdown" trigger="click" placement="bottom" :options="nestedOptions" tree-mode reset-tree-on-change :width="280">
    <HLButton size="sm">Tree (resets on close)</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// nestedOptions defined below (Dropdown Options tab)
</script>
ts
const nestedOptions = [
  {
    key: 'produce',
    label: 'Fresh Produce',
    children: [
      {
        key: 'fruits',
        label: 'Fruits & Berries',
        children: [
          { key: 'mango', label: 'Mango', description: 'Sweet and juicy' },
          { key: 'pineapple', label: 'Pineapple', description: 'Tangy and tropical' },
          { key: 'papaya', label: 'Papaya', description: 'Soft and sweet' },
        ],
      },
      {
        key: 'vegetables',
        label: 'Vegetables',
        children: [
          { key: 'spinach', label: 'Spinach', description: 'Dark and nutritious' },
          { key: 'kale', label: 'Kale', description: 'Crispy and healthy' },
          { key: 'lettuce', label: 'Lettuce', description: 'Fresh and crisp' },
        ],
      },
    ],
  },
  {
    key: 'dairy',
    label: 'Dairy & Eggs',
    children: [
      { key: 'whole_milk', label: 'Whole Milk', description: 'Full fat and creamy' },
      { key: 'cheddar', label: 'Cheddar', description: 'Sharp and aged' },
      { key: 'greek', label: 'Greek Yogurt', description: 'Thick and protein-rich' },
    ],
  },
]

Trigger Attributes

Pass triggerAttrs to add attributes to the trigger wrapper — useful for aria-*, data-*, or test hooks.

vue
<template>
  <HLDropdown
    id="trigger-attrs-dropdown"
    trigger="click"
    placement="bottom"
    :options="simpleOptions"
    :trigger-attrs="{ 'data-testid': 'actions-menu', 'aria-label': 'Document actions' }"
    :show-search="false"
    :width="200"
  >
    <HLButton size="sm">With Trigger Attrs</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

// simpleOptions defined below (Dropdown Options tab)
</script>
ts
const simpleOptions = [
  { key: 'edit', label: 'Edit Document' },
  { key: 'share', label: 'Share with Team' },
  { key: 'download', label: 'Download as PDF' },
  { key: 'duplicate', label: 'Make a Copy' },
  { key: 'archive', label: 'Archive Document' },
  { key: 'delete', label: 'Delete Document' },
]

Virtual Scroll

For very large flat lists, set virtualScroll so only the visible rows are rendered in the DOM, keeping the menu fast. It applies to flat lists only (not nested/tree options) and defaults maxHeight to 320px when unset.

vue
<template>
  <HLDropdown id="virtual-scroll-dropdown" trigger="click" placement="bottom" :options="virtualOptions" virtual-scroll :show-search="false" :width="240">
    <HLButton size="sm">Virtual Scroll (1000 items)</HLButton>
  </HLDropdown>
</template>

<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'

const virtualOptions = Array.from({ length: 1000 }, (_, i) => ({
  key: `v-${i + 1}`,
  label: `Item ${i + 1}`,
}))
</script>

Accessibility

  • Trigger: Pass id (or rely on the auto-generated id) so the component can assign stable ids: {id}-trigger on the trigger wrapper and {id}-menu on the menu panel. Add aria-haspopup="menu" and :aria-expanded directly on your trigger element (e.g., HLButton) inside the default slot. The menu root uses aria-labelledby pointing at the trigger id.
  • Menu panel: The list surface is exposed as role="menu" with aria-orientation="vertical", tabindex="-1" and aria-labelledby referencing the trigger.
  • Options: Rows use appropriate roles (menuitem, menuitemcheckbox / menuitemradio when selection marks apply), tabindex on focusable rows, and aria-disabled when an option is disabled. Disabled options are not interactive and do not open cascade submenus.
  • You can point to helper or validation text with aria-describedby on the trigger via triggerAttrs, and surface async loading with aria-busy when needed.

Props

PropTypeDefaultDescription
idstringAuto (hr-dropdown-*)Unique identifier for the dropdown. Omitted values receive a stable auto-generated id for accessibility and testing.
optionsDropdownOption[][]Array of options to display in the dropdown menu. See Option Properties for details.
trigger'click' | 'hover' | 'manual''click'How the menu opens. manual opens only via v-model:show / show (no pointer trigger). The focus trigger is not supported.
placement'top-start' | 'top' | 'top-end' | 'right-start' | 'right' | 'right-end' | 'bottom-start' | 'bottom' | 'bottom-end' | 'left-start' | 'left' | 'left-end''bottom-start'Position of the dropdown menu relative to the trigger element.
widthnumber | 'auto'182Width of the dropdown menu. Use a number for fixed pixels, or 'auto' to match the trigger width.
showboolean | undefinedundefinedControlled visibility state. Use with v-model:show for two-way binding.
showArrowbooleantrueShows a pointing arrow from the menu to the trigger.
showSelectedMarkbooleanfalseShows a checkmark next to the selected option.
treeModebooleanfalseEnables hierarchical navigation for nested options.
showSearchbooleantrueShows a search input at the top of the dropdown.
multiplebooleanfalseEnables multiple selection mode.
showCheckboxbooleanfalseRenders a checkbox before each leaf option. Intended for multiple selection with closeOnSelect: false.
virtualScrollbooleanfalseVirtualizes the options list so only visible rows are rendered. Only supported on flat lists
closeOnSelectbooleantrueControls whether the dropdown should close after an option is selected. Selected Mark is not shown if this is set to false.
resetTreeOnChangebooleanfalseControls whether the dropdown tree should reset the tree state when the dropdown is closed.
disabledbooleanfalseDisables the entire dropdown, preventing it from being opened.
maxHeightstring | undefinedundefinedMaximum height of the dropdown menu. Makes the dropdown scrollable when content exceeds this height.
showSearchHighlightbooleanfalseHighlights matching text in option labels when searching.
triggerAttrsHTMLAttributes{}Additional attributes to pass to the dropdown trigger wrapper (e.g., aria-*, data-*).
clearableInSearchbooleanfalseWhether the search input is clearable.
highlightSelectionbooleantrueHighlights the selected option.
searchPlaceholderstring'Search'Placeholder for the search input.
searchValuestring | undefinedundefinedControlled search query. Use with v-model:searchValue (pairs with the @update:searchValue event).
popoverPropsHLPopoverProps{}Additional HLPopover props applied to the root menu popover and cascade submenu popovers.
PropertyTypeDescriptionExample
keystringUnique identifier for the option.'edit'
labelstringDisplay text for the option.'Edit Post'
type'default' | 'header' | 'divider' | 'avatar' | 'icon' | 'render' | 'search'Type of menu item to render.'header'
descriptionstringSecondary text shown below the label.'Modify post content'
descriptionIcon() => VNodeIcon rendered next to description.() => h(InfoIcon)
icon() => VNodeIcon for the option.() => h(EditIcon)
iconPlacement'left' | 'right'Position of the icon.'left'
childrenDropdownOption[]Nested options for tree navigation.[{ key: 'sub1', label: 'Sub Option' }]
srcstringImage URL for avatar type options.'/path/to/image.png'
infoTextstringAdditional text shown to the right.'+1'
titleRightSlot() => VNodeCustom content for the right side.() => h(HLTag, { ... })
render() => VNodeCustom render function for the entire option.() => h('div', { ... })
disabledbooleanDisables the option, preventing selection.true
classstringCustom CSS class to apply to the option element.'custom-option'
hrefstringURL for link-type options. When set, the option renders as an <a> tag.'https://example.com'
targetstringTarget attribute for link-type options (e.g., '_blank').'_blank'
childrenMaxHeightstringMaximum height for the submenu displaying this option's children. Falls back to parent dropdown's maxHeight if not specified.'200px'

Emits

EventArgumentsDescription
@select(key: string | number, option: DropdownOption)Fired when an option is selected.
@update:show(show: boolean)Fired when dropdown visibility changes.
@search(value: string)Fired when the search input changes. Backed by the onSearch prop, so @search="..." and :on-search="..." are equivalent.
@update:searchValue(value: string)Fired when search value changes (for controlled search).
@clickoutside(event: Event)Fired when an outside interaction closes the dropdown.
@scroll(event: Event)Fired when the dropdown is scrolled (requires maxHeight to be set).

Slots

NameParametersDescription
default()The trigger element (default slot).
header()Content displayed at the top of the dropdown menu.
footer()Content displayed at the bottom of the dropdown menu.
empty()Content displayed when no options match the search query.
option-renderer{ option: DropdownOption }Custom renderer for individual options.
loader()Loading indicator displayed during infinite scroll or async operations.