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:
<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>// 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:
<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>// 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
<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>// 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.
<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>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.
<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>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:
<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>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' },
],
},
],
},
],
},
]Basic search
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
labelproperty - Displays the full path for nested options (e.g., "Parent / Child")
- Can be disabled by setting
show-searchtofalse
<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>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.
<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>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.
<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>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-searchenabled 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 whatoptionsholds. - Debounce the handler so you only call the API after the user pauses typing, rather than on every keystroke.
- Use the
loaderslot to show a spinner while the request is in flight, and theemptyslot 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:
<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>// 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:
Default Text Option - Simple text options with a key and a label.
ts{ key: 'default', label: 'Default Option' }Header - Used to group options together.
ts{ key: 'header1', label: 'Group 1', type: 'header' }Divider - Used to separate options.
ts{ key: 'divider1', type: 'divider' }Avatar - Displays an image.
ts{ key: 'avatar1', label: 'User Profile', type: 'avatar', src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=John' }Icon - Displays an icon. You can also set
iconPlacementto 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' }Description with Icon - Displays a description with an icon
ts{ key: 'desc1', label: 'Share Post', description: 'Share to social media channels', descriptionIcon: CheckVerified01Icon }Info Text - Displays additional text to the right of the label
ts{ key: 'info1', label: 'Messages', infoText: '5 unread' }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%' }) }Disabled - Disables an option
ts{ key: 'disabled1', label: 'Unavailable Feature', disabled: true }Render - Fully custom option content via a
renderfunction, for layouts the other types don't cover (rich cards, multi-line content, embedded components). The function returns a VNode built withh(). Note thattype: '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' }), ]), }
<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>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',
},
],
},
]Header, Footer & Empty Slots
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.
<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 & Empty</HLButton>
</HLDropdown>
</template>
<script setup lang="ts">
import { HLDropdown, HLButton } from '@platform-ui/highrise'
// simpleOptions defined below (Dropdown Options tab)
</script>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.
<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>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:
<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>// 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.
<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>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' },
],
},
]Submenu Height (Cascade Mode)
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.
childrenMaxHeightis set on parent options that have children- Each submenu can have its own unique max height
- If
childrenMaxHeightis not specified, the submenu falls back to the parent dropdown'smaxHeightprop - This is particularly useful for deeply nested menus where different levels need different scroll heights
<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>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.
<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>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
@scrollevent fires when the dropdown is scrolled (requiresmaxHeightto be set) - Use the
loaderslot 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.
<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>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.
<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>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.
<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>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.
<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>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.
<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>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.
<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>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.
<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>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.
<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>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.
<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}-triggeron the trigger wrapper and{id}-menuon the menu panel. Addaria-haspopup="menu"and:aria-expandeddirectly on your trigger element (e.g.,HLButton) inside the default slot. The menu root usesaria-labelledbypointing at the trigger id. - Menu panel: The list surface is exposed as
role="menu"witharia-orientation="vertical",tabindex="-1"andaria-labelledbyreferencing the trigger. - Options: Rows use appropriate roles (
menuitem,menuitemcheckbox/menuitemradiowhen selection marks apply),tabindexon focusable rows, andaria-disabledwhen 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-describedbyon the trigger viatriggerAttrs, and surface async loading witharia-busywhen needed.
Props
Dropdown Props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | Auto (hr-dropdown-*) | Unique identifier for the dropdown. Omitted values receive a stable auto-generated id for accessibility and testing. |
| options | DropdownOption[] | [] | 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. |
| width | number | 'auto' | 182 | Width of the dropdown menu. Use a number for fixed pixels, or 'auto' to match the trigger width. |
| show | boolean | undefined | undefined | Controlled visibility state. Use with v-model:show for two-way binding. |
| showArrow | boolean | true | Shows a pointing arrow from the menu to the trigger. |
| showSelectedMark | boolean | false | Shows a checkmark next to the selected option. |
| treeMode | boolean | false | Enables hierarchical navigation for nested options. |
| showSearch | boolean | true | Shows a search input at the top of the dropdown. |
| multiple | boolean | false | Enables multiple selection mode. |
| showCheckbox | boolean | false | Renders a checkbox before each leaf option. Intended for multiple selection with closeOnSelect: false. |
| virtualScroll | boolean | false | Virtualizes the options list so only visible rows are rendered. Only supported on flat lists |
| closeOnSelect | boolean | true | Controls whether the dropdown should close after an option is selected. Selected Mark is not shown if this is set to false. |
| resetTreeOnChange | boolean | false | Controls whether the dropdown tree should reset the tree state when the dropdown is closed. |
| disabled | boolean | false | Disables the entire dropdown, preventing it from being opened. |
| maxHeight | string | undefined | undefined | Maximum height of the dropdown menu. Makes the dropdown scrollable when content exceeds this height. |
| showSearchHighlight | boolean | false | Highlights matching text in option labels when searching. |
| triggerAttrs | HTMLAttributes | {} | Additional attributes to pass to the dropdown trigger wrapper (e.g., aria-*, data-*). |
| clearableInSearch | boolean | false | Whether the search input is clearable. |
| highlightSelection | boolean | true | Highlights the selected option. |
| searchPlaceholder | string | 'Search' | Placeholder for the search input. |
| searchValue | string | undefined | undefined | Controlled search query. Use with v-model:searchValue (pairs with the @update:searchValue event). |
| popoverProps | HLPopoverProps | {} | Additional HLPopover props applied to the root menu popover and cascade submenu popovers. |
DropdownOption Properties
| Property | Type | Description | Example |
|---|---|---|---|
| key | string | Unique identifier for the option. | 'edit' |
| label | string | Display text for the option. | 'Edit Post' |
| type | 'default' | 'header' | 'divider' | 'avatar' | 'icon' | 'render' | 'search' | Type of menu item to render. | 'header' |
| description | string | Secondary text shown below the label. | 'Modify post content' |
| descriptionIcon | () => VNode | Icon rendered next to description. | () => h(InfoIcon) |
| icon | () => VNode | Icon for the option. | () => h(EditIcon) |
| iconPlacement | 'left' | 'right' | Position of the icon. | 'left' |
| children | DropdownOption[] | Nested options for tree navigation. | [{ key: 'sub1', label: 'Sub Option' }] |
| src | string | Image URL for avatar type options. | '/path/to/image.png' |
| infoText | string | Additional text shown to the right. | '+1' |
| titleRightSlot | () => VNode | Custom content for the right side. | () => h(HLTag, { ... }) |
| render | () => VNode | Custom render function for the entire option. | () => h('div', { ... }) |
| disabled | boolean | Disables the option, preventing selection. | true |
| class | string | Custom CSS class to apply to the option element. | 'custom-option' |
| href | string | URL for link-type options. When set, the option renders as an <a> tag. | 'https://example.com' |
| target | string | Target attribute for link-type options (e.g., '_blank'). | '_blank' |
| childrenMaxHeight | string | Maximum height for the submenu displaying this option's children. Falls back to parent dropdown's maxHeight if not specified. | '200px' |
Emits
Dropdown Emits
| Event | Arguments | Description |
|---|---|---|
@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
| Name | Parameters | Description |
|---|---|---|
| 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. |