Advanced Select
A searchable dropdown select component with rich option rendering, supporting single selection, multiple selection with checkboxes, and tag-based selection. Built as a thin wrapper around the Dropdown component with trigger display via AvatarGroup or InputTag.
Single Select
Select a single option. The trigger displays the selected avatar. Click the selected option again to deselect.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
trigger-placeholder="Select a person"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(null)
const options = [
{ label: 'John Doe', key: '1', type: 'avatar', src: 'https://example.com/john.jpg' },
{ label: 'Jane Smith', key: '2', type: 'avatar', src: 'https://example.com/jane.jpg' },
{ label: 'Bob Wilson', key: '3', type: 'avatar' },
]
</script>Single Select with Preselected Value
Pass a value to preselect an option on mount.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref('1')
</script>Multiple Select
Enable multiple to allow selecting several options. The trigger shows an avatar group, and the dropdown shows checkboxes.
BW
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
multiple
trigger-placeholder="Select multiple people"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['1', '2', '3'])
</script>Tags Mode
Set select-type="tags" for tag-based selection. The trigger renders as an InputTag component with closable tags, and the dropdown shows pill-shaped options.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
select-type="tags"
trigger-placeholder="Add tags"
:default-tag-props="{ round: true }"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['bug', 'feature'])
const options = [
{ label: 'Bug', key: 'bug' },
{ label: 'Feature', key: 'feature' },
{ label: 'Documentation', key: 'docs' },
]
</script>Tags with Create
When show-add-tag-c-t-a is true and the search doesn't match any existing option, a "+ tag" button appears to create new tags on the fly.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
select-type="tags"
:show-add-tag-c-t-a="true"
:reset-search-string="true"
trigger-placeholder="Add or create tags"
search-placeholder="Search or create tags"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref([])
</script>Disabled
When disabled is set, the trigger is non-interactive and the dropdown cannot be opened.
<template>
<HLAdvancedSelect :options="options" :value="value" disabled />
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref('1')
</script>Loading State
When loading is true, a skeleton UI fills the dropdown. The skeleton adapts to the mode: avatar + text rows for default, pill shapes for tags.
Default mode
Tags mode
<template>
<!-- Default mode -->
<HLAdvancedSelect :options="options" :value="null" loading />
<!-- Tags mode -->
<HLAdvancedSelect :options="options" :value="[]" loading select-type="tags" />
</template>Dock Selected to Top
When dock-selected-to-top is true, selected options are sorted to the top of the list.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
:dock-selected-to-top="dockEnabled"
multiple
>
<template #header>
<div style="padding: 8px 12px; border-bottom: 1px solid var(--gray-200);">
<HLCheckbox v-model:checked="dockEnabled" size="sm">Show selected on top</HLCheckbox>
</div>
</template>
</HLAdvancedSelect>
</template>
<script setup lang="ts">
import { HLAdvancedSelect, HLCheckbox } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['5', '7'])
const dockEnabled = ref(false)
</script>No Search
Set show-search to false to hide the search input.
<template>
<HLAdvancedSelect :options="options" :value="value" @update:value="v => value = v" :show-search="false" />
</template>Virtual Scroll
Enable virtual-scroll for large option lists. Only visible items are rendered for performance.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
virtual-scroll
multiple
:popover-width="320"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const options = Array.from({ length: 200 }, (_, i) => ({
label: `Option ${i}`,
key: i,
type: 'avatar',
}))
const value = ref([])
</script>Remote Search
Set remote to true to handle search externally. The component emits @search instead of filtering locally. Update the options prop with results from your API. When using remote search with multiple/tags, merge currently-selected options into the list — see Design Guidelines.
<template>
<HLAdvancedSelect
:options="filteredOptions"
:value="value"
@update:value="v => value = v"
:loading="loading"
remote
multiple
@search="onSearch"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const loading = ref(false)
const filteredOptions = ref(allOptions)
const onSearch = (query: string) => {
loading.value = true
fetchOptions(query).then(results => {
filteredOptions.value = results
loading.value = false
})
}
</script>Scroll Pagination
Listen to @scroll to implement infinite loading. Append new options when the user scrolls near the bottom.
Page: 1 · Total: 10
<template>
<HLAdvancedSelect
:options="options"
:value="value"
@update:value="v => value = v"
@scroll="onScroll"
/>
</template>
<script setup lang="ts">
import { HLAdvancedSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const options = ref(fetchPage(1))
const onScroll = ({ clientHeight, scrollHeight, scrollTop }) => {
if (scrollTop + clientHeight >= scrollHeight - 10) {
const nextPage = fetchPage(currentPage++)
options.value = [...options.value, ...nextPage]
}
}
</script>Custom Trigger
Use the #trigger slot to render a completely custom trigger element.
<template>
<HLAdvancedSelect :options="options" :value="value" @update:value="v => value = v">
<template #trigger="{ disabled }">
<HLButton variant="primary" :disabled="disabled">
{{ selectedLabel }}
</HLButton>
</template>
</HLAdvancedSelect>
</template>
<script setup lang="ts">
import { HLAdvancedSelect, HLButton } from '@platform-ui/highrise'
import { ref, computed } from 'vue'
const value = ref('1')
const selectedLabel = computed(() => options.find(o => o.key === value.value)?.label ?? 'Select...')
</script>Header and Footer Slots
Use #header and #footer slots to add custom content above and below the options list.
BW
<template>
<HLAdvancedSelect :options="options" :value="value" @update:value="v => value = v" multiple>
<template #header>
<div>{{ value.length }} selected</div>
</template>
<template #footer>
<HLButton @click="value = []">Clear all</HLButton>
<HLButton>Apply</HLButton>
</template>
</HLAdvancedSelect>
</template>
<script setup lang="ts">
import { HLAdvancedSelect, HLButton } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['1', '2', '3'])
</script>Option Types
Options extend HLDropdownOption. Use type: 'avatar' for avatar rendering with initials fallback, or omit type for text-only options.
const options = [
// Avatar with image
{ key: '1', label: 'John Doe', type: 'avatar', src: 'https://example.com/john.jpg' },
// Avatar with initials (no src)
{ key: '2', label: 'Jane Smith', type: 'avatar' },
// With description
{ key: '3', label: 'Bob Wilson', type: 'avatar', description: '[email protected]' },
// Disabled option
{ key: '4', label: 'Disabled', type: 'avatar', disabled: true },
// Text-only option (no avatar)
{ key: '5', label: 'Plain text option' },
]Custom Option Rendering
Use the #option-renderer slot to fully control how each option is rendered. The slot receives { option } (with option.selected reflecting the current selection) and replaces the default row layout.
The slot replaces the default click handler too — wire @click to your own selection logic and update the bound value yourself.
Default Avatar Type
Hover the avatar group in the trigger to remove selections via the × button; the value flows back through @update:value.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
multiple
trigger-placeholder="Pick teammates"
:show-search="false"
@update:value="v => value = v"
>
<template #option-renderer="{ option }">
<div
:style="{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px 12px',
cursor: option.disabled ? 'not-allowed' : 'pointer',
background: option.selected ? 'var(--primary-50, #eff6ff)' : 'transparent'
}"
@click="!option.disabled && toggle(option.key)"
>
<HLAvatar :src="option.src" :name="option.label" size="sm" round />
<div :style="{ flex: 1, minWidth: 0 }">
<div :style="{ display: 'flex', alignItems: 'center', gap: '6px' }">
<HLText size="sm" weight="semibold">{{ option.label }}</HLText>
<HLTag size="xs" :color="roleColor(option.meta?.role)" :bordered="false" :interactive="false" round>
{{ option.meta?.role }}
</HLTag>
</div>
<HLText size="xs" :style="{ color: 'var(--gray-500)' }">{{ option.description }}</HLText>
</div>
<HLBadge v-if="option.meta?.unread" :value="option.meta.unread" type="error" />
</div>
</template>
</HLAdvancedSelect>
</template>
<script setup lang="ts">
import { HLAdvancedSelect, HLAvatar, HLBadge, HLTag, HLText } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['u-2'])
const options = [
{ key: 'u-1', label: 'John Doe', description: '[email protected]', src: 'https://i.pravatar.cc/150?u=john', meta: { role: 'Admin', unread: 3 } },
{ key: 'u-2', label: 'Jane Smith', description: '[email protected]', src: 'https://i.pravatar.cc/150?u=jane', meta: { role: 'Member', unread: 0 } },
{ key: 'u-3', label: 'Bob Wilson', description: '[email protected]', meta: { role: 'Member', unread: 12 } },
]
const roleColor = (role: string) => ({ Admin: 'primary', Member: 'gray', Guest: 'warning' }[role] || 'gray')
const toggle = (key: string) => {
value.value = value.value.includes(key)
? value.value.filter(v => v !== key)
: [...value.value, key]
}
</script>Tag-mode override with icon and color
When select-type="tags" is set, the component renders pill-shaped options by default. Providing #option-renderer replaces that default — useful for surfacing per-option color or icon metadata.
<template>
<HLAdvancedSelect
:options="options"
:value="value"
select-type="tags"
trigger-placeholder="Pick labels"
:show-search="false"
@update:value="v => value = v"
>
<template #option-renderer="{ option }">
<div
:style="{
display: 'flex',
alignItems: 'center',
gap: '8px',
padding: '6px 12px',
cursor: 'pointer',
background: option.selected ? 'var(--primary-50, #eff6ff)' : 'transparent'
}"
@click="toggle(option.key)"
>
<span>{{ option.meta?.icon }}</span>
<HLTag size="xs" :color="option.meta?.color" :bordered="false" :interactive="false" round>
{{ option.label }}
</HLTag>
</div>
</template>
</HLAdvancedSelect>
</template>
<script setup lang="ts">
import { HLAdvancedSelect, HLTag } from '@platform-ui/highrise'
import { ref } from 'vue'
const value = ref(['feature'])
const options = [
{ key: 'bug', label: 'Bug', meta: { color: 'error', icon: '🐞' } },
{ key: 'feature', label: 'Feature', meta: { color: 'success', icon: '✨' } },
{ key: 'docs', label: 'Docs', meta: { color: 'primary', icon: '📚' } },
]
const toggle = (key: string) => {
value.value = value.value.includes(key)
? value.value.filter(v => v !== key)
: [...value.value, key]
}
</script>option.selectedreflects the current selection state, so use it to drive highlight/check visuals.- Wire
@clickto your own toggle logic — the slot fully replaces the default row, including its built-in click handling. - The slot only governs dropdown rows. The trigger (avatar group or input tag) keeps its built-in remove (
×) behavior and emits the resulting array via@update:value— always bind that handler so removals from the trigger flow back into your bound value.
Design Guidelines
- Always set
popover-width(a number, or'trigger'to match the trigger) whenvirtual-scrollis enabled. Because rows are rendered on demand and measured as they enter the viewport, an unconstrained dropdown will resize horizontally while scrolling as wider rows mount in — set the width up front so the layout stays stable.
<HLAdvancedSelect
:options="largeOptions"
virtual-scroll
:popover-width="320"
/>Accessibility
- The dropdown renders with
role="listbox"andaria-orientation="vertical" - Options support Enter/Space to select and Escape to close
- Disabled options have
aria-disabled="true"and are not focusable - Multiple mode options have
aria-checkedreflecting selection state - Search input auto-focuses when the dropdown opens
Imports
import { HLAdvancedSelect } from '@platform-ui/highrise'
import type { HLAdvancedSelectOption, HLAdvancedSelectProps } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| value | string | number | (string | number)[] | null | undefined | Selected value(s). Use with @update:value or v-model:value. |
| options | HLAdvancedSelectOption[] | [] | Array of options. Extends HLDropdownOption with children and meta. |
| multiple | boolean | false | Enable multiple selection with checkboxes. |
| selectType | 'default' | 'tags' | 'default' | 'tags' enables tag-based selection with InputTag trigger. |
| remote | boolean | false | Emit @search instead of filtering locally. |
| loading | boolean | undefined | Show skeleton loading UI in dropdown. |
| showSearch | boolean | true | Show or hide the search input. |
| searchPlaceholder | string | 'Search' | Placeholder for the search input. |
| triggerPlaceholder | string | 'Search' | Placeholder when nothing is selected. |
| maxAvatarCount | number | 5 | Max avatars in trigger before +N overflow. |
| virtualScroll | boolean | false | Enable virtual scrolling for large lists. Recommended alongside a fixed popoverWidth for a stable dropdown size. |
| maxHeight | string | '320px' | Max height of the dropdown options area. |
| dockSelectedToTop | boolean | false | Sort selected options to top. |
| defaultTagProps | Partial<HLTagProps> | undefined | Styling for tags in the InputTag trigger (e.g. { round: true }). |
| defaultAvatarProps | Partial<HLAvatarProps> | undefined | Avatar props for the trigger (e.g. { size: 'md' }). |
| show | boolean | undefined | Controlled popover visibility. Omit for uncontrolled. |
| placement | HLPopoverPlacement | 'bottom-start' | Dropdown placement relative to trigger. |
| showArrow | boolean | false | Show popover arrow. |
| to | string | HTMLElement | false | undefined | Teleport target for the dropdown. |
| disabled | boolean | false | Disable the component. |
| showAddTagCTA | boolean | true | Show "+ tag" button when search has no matches (tags mode). |
| handleNewOptionRemote | boolean | false | Emit @newTag instead of creating the tag locally. |
| resetSearchString | boolean | false | Clear search when value changes. |
| popoverWidth | number | 'trigger' | undefined | Fixed width or match trigger width. Tags mode always matches trigger. |
| popoverContainerClass | string | undefined | Additional CSS class on the dropdown container. |
Emits
| Name | Parameters | Description |
|---|---|---|
@update:value | (value: string | number | (string | number)[] | null) | Selection changed. |
@update:show | (show: boolean) | Popover visibility changed. |
@search | (query: string) | Search input changed (only when remote is true). |
@scroll | ({ clientHeight, scrollHeight, scrollTop }) | Options area scrolled. Use for infinite loading. |
@close | () | Dropdown closed. |
@newTag | (label: string) | New tag requested (when handleNewOptionRemote is true). |
Slots
| Name | Props | Description |
|---|---|---|
trigger | { disabled, value, placeholder } | Custom trigger element. |
header | - | Sticky content above the search/options. |
footer | - | Sticky content below the options. |
empty | - | Shown when search has no results. |
emptySearch | { searchString } | Alternative to empty with access to the search query. |
option-renderer | { option } | Custom rendering for each option. |
loader | - | Content at the bottom of options list (for infinite scroll). |
Best Practices
- Always use
keyas the unique identifier for options (inherited fromHLDropdownOption) - Set
type: 'avatar'on options that should render with an avatar (image or initials fallback) - Use
remotemode for server-side filtering — updateoptionswith API results on@search - Use
virtual-scrollfor lists with more than 50 options - Prefer
#headerand#footerslots for consistent sticky positioning - Use
dock-selected-to-topwithmultiplemode for better UX with many options