Select
Select component for choosing single or multiple options
Default Select
Single-select dropdown bound to a flat array of options.
<template>
<HLSelect :options="simpleSelectOptions" :value="selectedValue" @update:value="handleSimpleChange" />
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const selectedValue = ref(null)
const simpleSelectOptions = [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
]
const handleSimpleChange = value => {
selectedValue.value = value
}
</script>With Icon
Renders a leading icon in the trigger via the icon slot with type="avatar".
<template>
<HLSelect type="avatar" :options="options" :value="selectedValue" @update:value="handleChange">
<template #icon>
<div class="hr-select-menu-placeholder-icon">
<User01Icon />
</div>
</template>
</HLSelect>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { User01Icon } from '@gohighlevel/ghl-icons/24/outline'
import { ref } from 'vue'
import { options } from './options'
const selectedValue = ref('')
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
type: 'group',
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label: 'Drive My Car',
value: 'song1',
description: 'Drive My Car.ogg',
tagColor: 'blue',
},
{
label: 'Norwegian Wood',
value: 'song2',
description: 'Norwegian Wood.ogg',
tagColor: 'green',
},
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
description:
'lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.',
tagColor: 'blue',
},
// ... other songs
],
},
{
type: 'group',
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
value: 'Two Of Us',
tagColor: 'purple',
},
{
label: 'Dig A Pony',
value: 'Dig A Pony',
description: 'Dig A Pony.avi',
tagColor: 'orange',
},
// ... other songs
],
},
]Searchable Select
Enables in-dropdown text filtering with filterable and a search icon via showSearchIcon.
<template>
<HLSelect filterable showSearchIcon :options="options" :value="selectedValue" @update:value="handleChange" />
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
import { options } from './options'
const selectedValue = ref('')
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
type: 'group',
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label: 'Drive My Car',
value: 'song1',
description: 'Drive My Car.ogg',
tagColor: 'blue',
},
{
label: 'Norwegian Wood',
value: 'song2',
description: 'Norwegian Wood.ogg',
tagColor: 'green',
},
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
description:
'lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.',
tagColor: 'blue',
},
// ... other songs
],
},
{
type: 'group',
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
value: 'Two Of Us',
tagColor: 'purple',
},
{
label: 'Dig A Pony',
value: 'Dig A Pony',
description: 'Dig A Pony.avi',
tagColor: 'orange',
},
// ... other songs
],
},
]Empty Options State
Use an empty array to render the built-in empty state while keeping the trigger interactive.
<template>
<HLSelect
id="select-empty-options"
:options="[]"
placeholder="No options available"
aria-label="Empty select example"
/>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
</script>Loading
Set loading to show a loading indicator in the select.
<template>
<HLSelect :options="options" :loading="true" />
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]
</script>Clearing the value
When you want to clear the value of the select, you can set the value to null. Setting the value to undefined will create inconsistent behavior.
<template>
<HLSelect :options="options" v-model:value="selectedValue2" />
<HLButton @click="selectedValue2 = null">Clear Value</HLButton>
</template>
<script setup lang="ts">
import { HLSelect, HLButton } from '@platform-ui/highrise'
import { ref } from 'vue'
const selectedValue2 = ref('')
const options = ref([
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
])
</script>Multiple Selection
Allows selecting multiple values, rendering each choice as a removable tag.
<template>
<HLSelect
multiple
type="avatar"
filterable
showSearchIcon
:options="options"
:value="selectedValue"
:showAvatarInTags="false"
@update:value="handleChange"
id="select-multiple"
/>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
import { options } from './options'
const selectedValue = ref('')
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
type: 'group',
label: 'Rubber Soul',
key: 'Rubber Soul',
children: [
{
label: 'Drive My Car',
value: 'song1',
description: 'Drive My Car.ogg',
tagColor: 'blue',
},
{
label: 'Norwegian Wood',
value: 'song2',
description: 'Norwegian Wood.ogg',
tagColor: 'green',
},
{
label: "Everybody's Got Something to Hide Except Me and My Monkey",
value: 'song0',
description:
'lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.lorsum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam. Loresum ipsum dolor sit amet, consectetur adipiscing elit. Nulla nec purus feugiat, molestie ipsum et, consequat nunc. Nulla facilisi. Nullam.',
tagColor: 'blue',
},
// ... other songs
],
},
{
type: 'group',
label: 'Let It Be',
key: 'Let It Be Album',
children: [
{
label: 'Two Of Us',
value: 'Two Of Us',
tagColor: 'purple',
},
{
label: 'Dig A Pony',
value: 'Dig A Pony',
description: 'Dig A Pony.avi',
tagColor: 'orange',
},
// ... other songs
],
},
]Multiple Selection without Avatar
Multi-select with plain text tags by setting showAvatarInTags to false.
<template>
<HLSelect
multiple
filterable
showSearchIcon
:options="simpleMultiSelectOptions"
:value="selectedValue"
@update:value="handleSimpleMultiChange"
id="select-multiple"
:showAvatarInTags="false"
/>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
import { simpleMultiSelectOptions } from './options'
const selectedValue = ref('')
const handleSimpleMultiChange = value => {
selectedValue.value = value
}
</script>export const simpleMultiSelectOptions = [
{
label: 'Option 1 with a long label to test the overflow',
value: 'option1',
},
{
label: 'Option 2 with a long label to test the overflow',
value: 'option2',
},
{
label: 'Option 3 with a long label to test the overflow',
value: 'option3',
},
{
label: 'Option 4',
value: 'option4',
},
{
label: 'Option 5',
value: 'option5',
},
{
label: 'Option 6',
value: 'option6',
},
]Tag Truncation
When using multiple selection with long tag labels, you can enable tag truncation to prevent tags from overflowing their container. This is particularly useful when dealing with lengthy option labels.
<template>
<HLSelect
multiple
filterable
showSearchIcon
:allowTagTruncation="true"
:maxTagWidth="120"
:options="options"
:value="selectedValue"
@update:value="handleChange"
id="select-truncated"
:showAvatarInTags="false"
/>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
import { options } from './options'
const selectedValue = ref('')
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
label: 'Option 1 with a very long label that would normally overflow',
value: 'option1',
},
{
label: 'Option 2 with another extremely long label for testing truncation',
value: 'option2',
},
{
label: 'Option 3 with yet another long label to demonstrate truncation behavior',
value: 'option3',
},
{
label: 'Short option',
value: 'option4',
},
]Remote Search
Use remote search when you need to fetch options dynamically from an API or when dealing with large datasets that should be filtered server-side.
INFO
remote requires filterable to be set as well. The @search event (which you handle to fetch options) only fires while the search input is active, and that input is only rendered when filterable is true. Handle @search to load options and drive the loading state; the component does not filter locally in remote mode.
<template>
<HLSelect
id="remote-search"
:value="selectedValue"
:options="filteredOptions"
:loading="loading"
:filterable="true"
:remote="true"
placeholder="Type to search..."
aria-label="Remote search select example"
@search="handleSearch"
@update:value="handleChange"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
const selectedValue = ref(null)
const loading = ref(false)
const filteredOptions = ref([])
const handleSearch = async query => {
loading.value = true
try {
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000))
// Filter results if there's a query, otherwise show all
filteredOptions.value =
query && query.trim()
? InfiniteScrollOptions.value.filter(option => option.label.toLowerCase().includes(query.toLowerCase().trim()))
: InfiniteScrollOptions.value
} catch (error) {
console.error('Search failed:', error)
filteredOptions.value = []
} finally {
loading.value = false
}
}
// Load initial options
handleSearch('')
const handleChange = (value, option) => {
selectedValue.value = value
console.log('Selected:', { value, option })
}
</script>Infinite Scroll
Appends more options as the user scrolls to the bottom via the scroll event, keeping the menu open with reset-menu-on-options-change="false".
<template>
<HLSelect
:options="InfiniteScrollOptions"
:value="infiniteSelectedValue"
@update:value="handleInfiniteChange"
@scroll="handleScroll"
:reset-menu-on-options-change="false"
:loading="infiniteLoading"
/>
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const infiniteSelectedValue = ref(null)
const infiniteLoading = ref(false)
const handleInfiniteChange = value => {
infiniteSelectedValue.value = value
}
const handleScroll = async event => {
const scrollPosition = event.target.scrollTop + event.target.clientHeight
if (scrollPosition >= event.target.scrollHeight - 10) {
infiniteLoading.value = true
const result = await fetchOptions() // fetch API
InfiniteScrollOptions.value = [...InfiniteScrollOptions.value, ...result]
infiniteLoading.value = false
}
}
const InfiniteScrollOptions = ref([
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
])
</script>Advanced Tag Configuration with tagProps
The new tagProps object provides complete control over tag rendering in multiple selection mode.
Basic Usage
Each option carries its own tagProps, so tags in the same select can differ in color, shape, and behaviour. Note the disabled tag has no close button, and the third tag truncates at maxWidth.
<template>
<HLSelect multiple :options="options" :value="selectedValue" @update:value="handleChange" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
import { options } from './options'
const selectedValue = ref(['option1', 'option2', 'option3', 'option4'])
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
label: 'Success Tag',
value: 'option1',
tagProps: {
color: 'success',
round: true,
},
},
{
label: 'Interactive Tag',
value: 'option2',
tagProps: {
color: 'primary',
interactive: true,
count: 3,
},
},
{
label: 'Truncated Tag with a very long label',
value: 'option3',
tagProps: {
color: 'blue',
truncate: true,
maxWidth: 120,
},
},
{
label: 'Disabled Tag',
value: 'option4',
tagProps: {
color: 'gray',
disabled: true,
closable: false,
},
},
]Available tagProps Options
| Property | Type | Description | Default |
|---|---|---|---|
color | HLTagColor | Tag color variant | 'gray' |
size | 'lg' | 'md' | 'sm' | 'xs' | Tag size | Based on select size |
round | boolean | Rounded appearance | false |
bordered | boolean | Show border | true |
interactive | boolean | Interactive states | true |
disabled | boolean | Disabled state | false |
closable | boolean | Show close button | true |
count | number | Display count in tag | undefined |
dropdown | 'open' | 'close' | false | Dropdown indicator | false |
truncate | boolean | Truncate long text | false |
maxWidth | string | number | Max width for truncation | 136 |
avatarProps | HLAvatarProps | Avatar configuration | undefined |
Avatar Configuration
The avatarProps property customizes the avatar shown inside each tag. Pass src for an image, or name to fall back to initials.
INFO
Tag avatars only render when the select has type="avatar" and showAvatarInTags is left true (the default). With the default type="default", avatarProps is ignored.
<template>
<!-- type="avatar" is required for tag avatars to render -->
<HLSelect multiple type="avatar" :options="options" :value="selectedValue" @update:value="handleChange" />
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
import { options } from './options'
const selectedValue = ref(['user1', 'user2'])
const handleChange = value => {
selectedValue.value = value
}
</script>export const options = [
{
label: 'John Doe',
value: 'user1',
tagProps: {
color: 'primary',
avatarProps: {
size: 'xs', // Avatar size
objectFit: 'cover', // How the image fits in the avatar
round: true, // Rounded avatar
src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=John', // Avatar image URL
},
},
},
{
// No src — the avatar falls back to initials derived from `name`
label: 'Jane Smith',
value: 'user2',
tagProps: {
color: 'blue',
avatarProps: {
size: 'xs',
round: true,
name: 'Jane Smith',
},
},
},
]INFO
name is only used when avatarProps.src is absent — if you pass both, src wins and name is dropped. When neither is set, the avatar falls back to the option's label.
Migration from tagColor
The tagColor prop is deprecated but still supported for backward compatibility:
// Old way (deprecated but still works)
{
label: "Option",
value: "opt1",
tagColor: "primary"
}
// New way (recommended)
{
label: "Option",
value: "opt1",
tagProps: {
color: "primary"
}
}Teleport Select
By default, the select dropdown menu is rendered within its parent component's DOM tree. However, this can cause styling and positioning issues, especially when the select is used inside containers with overflow: hidden, fixed positioning, or complex z-index stacking contexts (like modals, dialogs, or fixed sidebars).
The to prop allows you to teleport (move) the dropdown menu to a different location in the DOM tree, outside of its parent component. This ensures the dropdown remains visible and correctly positioned, regardless of its parent container's styling constraints.
You can specify the target location using:
- A CSS selector (e.g.,
to="#select-teleport-target") falseto disable teleportingtrueto teleport to the default location (body)
<template>
<div id="select-teleport-target"></div>
<HLSelect :options="options" :value="selectedValue" @update:value="handleChange" to="#select-teleport-target" id="select-teleport" />
</template>
<script setup lang="ts">
import { HLSelect } from '@platform-ui/highrise'
import { ref } from 'vue'
const selectedValue = ref(null)
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]
const handleChange = value => {
selectedValue.value = value
}
</script>Custom Rendering
The select component supports powerful custom rendering capabilities. This enables you to customize how options appear in the dropdown menu and how selected values are displayed as tags. We recommend using the option and tag slots—they are the preferred way to pass custom option and tag content. The optionRenderer prop and option-level tagRenderer remain supported for backward compatibility but are planned for deprecation.
Slot-based Rendering (Recommended)
optionslot: Customizes how each option is rendered in the dropdown menu.tagslot: Customizes the content rendered inside each selected tag in multiple selection mode, and the selected value in single selection mode.
Note
The slot content does not replace the tag itself. The tag component remains the wrapper, and option.tagProps still applies to that wrapper. Use the slot only for the inner content.
<template>
<HLSelect
id="select-slot-renderers"
multiple
filterable
showSearchIcon
:options="slotRenderOptions"
:value="slotSelectedValue"
:maxTagCount="'responsive'"
aria-label="Select using slot renderers"
@update:value="handleSlotRenderChange"
>
<template #option="{ option, selected, disabled }">
<div class="flex items-center justify-between w-full gap-2">
<div class="flex flex-col">
<HLText size="sm" :class="disabled ? 'text-gray-400' : 'text-gray-900'">
{{ option.label }}
</HLText>
<HLText v-if="option.description" size="xs" class="text-gray-500">
{{ option.description }}
</HLText>
</div>
<HLTag v-if="selected" size="xs" round color="success">Selected</HLTag>
</div>
</template>
<template #tag="{ option, disabled }">
<span class="flex items-center gap-1">
<HLIcon v-if="!disabled" size="xs"><CheckVerified01Icon /></HLIcon>
<span>{{ option.label }}</span>
</span>
</template>
</HLSelect>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect, HLTag, HLText, HLIcon } from '@platform-ui/highrise'
import { CheckVerified01Icon } from '@gohighlevel/ghl-icons/24/outline'
import { slotRenderOptions } from './options'
const slotSelectedValue = ref(['alpha'])
const handleSlotRenderChange = value => {
slotSelectedValue.value = value
}
</script>export const slotRenderOptions = [
{
label: 'Alpha',
value: 'alpha',
description: 'Primary choice',
},
{
label: 'Beta',
value: 'beta',
description: 'Secondary option',
disabled: true,
},
{
label: 'Gamma',
value: 'gamma',
description: 'Experimental option',
},
]Render function API (deprecated)
Deprecation
The optionRenderer prop and option-level tagRenderer are still supported but will be deprecated in a future release. Prefer the option and tag slots above for custom option and tag rendering.
Custom rendering API:
optionRenderer- Customizes how each option is rendered in the dropdown menu via a render function.tagRenderer- Customizes how the selected value is rendered in the trigger (single select) or inside the tag (multiple select) via a string, VNode, or render function. The render function should return a validVNodeChild; if it returnsundefinedornull, the option label is used as fallback.
<script setup lang="ts">
import { h, ref } from 'vue'
import { HLSelect, HLSpace, HLIcon, HLAvatar, HLTag, HLText } from '@platform-ui/highrise'
import { CheckVerified01Icon } from '@gohighlevel/ghl-icons/24/outline'
const selectedValue = ref(null)
const handleChange = value => {
selectedValue.value = value
}
const customOptions = [
{
type: 'group',
label: 'Custom Rendering Examples',
key: 'custom-group',
children: [
{
label: 'Custom Label with Tag',
value: 'option1',
tagRenderer: 'Custom Tag',
backgroundColor: 'var(--green-50)',
color: 'var(--green-700)',
renderOption: () =>
h('div', { class: 'flex items-center gap-2' }, [
h('div', 'Custom Label'),
h(HLTag, { size: 'sm', round: true, variant: 'error' }, { default: () => '2% increase' }),
]),
},
{
label: 'Verified Option',
value: 'option2',
description: 'With icon and description',
backgroundColor: 'var(--purple-50)',
color: 'var(--purple-700)',
renderOption: () =>
h(
HLSpace,
{ align: 'center', wrapItem: false },
{
default: () => [h(HLIcon, { size: 'sm' }, { default: () => h(CheckVerified01Icon) }), h('span', 'Verified Option')],
}
),
},
{
label: 'USA',
value: 'option3',
backgroundColor: 'var(--orange-50)',
color: 'var(--orange-700)',
renderOption: () =>
h(
HLSpace,
{ align: 'center', wrapItem: false, justify: 'space-between' },
{
default: () => [
h(
HLSpace,
{ align: 'center', wrapItem: false },
{
default: () => [
h(HLAvatar, {
size: 'xs',
src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=USA',
}),
h('span', 'USA'),
],
}
),
h(HLText, { size: 'xs' }, { default: () => '+1' }),
],
}
),
tagRenderer: () =>
h(
HLSpace,
{ align: 'center', wrapItem: false },
{
default: () => [
h('img', {
style: { width: '16px', height: '16px', borderRadius: '50%' },
src: 'https://api.dicebear.com/9.x/avataaars/svg?seed=USA',
}),
h('span', 'USA'),
],
}
),
},
],
},
]
const optionRenderer = option => {
if (option.renderOption) {
return option.renderOption()
}
return h('span', option.label)
}
</script>
<template>
<HLSelect
:options="customOptions"
:value="selectedValue"
@update:value="handleChange"
:option-renderer="optionRenderer"
multiple
filterable
id="select-custom-render"
/>
</template>Trigger and Menu Customization
Fine-tune the trigger and dropdown appearance: triggerFontSize / triggerFontWeight style the selected text, showArrow toggles the dropdown chevron, showCheckmark toggles the selected-option tick, optionHeight sets the row height, menuProps passes attributes to the dropdown menu, and wrapperClass adds a class to the wrapper.
<template>
<HLSelect
id="select-trigger-custom"
:options="options"
:value="value"
@update:value="val => (value = val)"
trigger-font-size="lg"
trigger-font-weight="semibold"
:show-arrow="true"
:show-checkmark="false"
:option-height="48"
:menu-props="{ 'data-testid': 'custom-menu' }"
wrapper-class="custom-select-wrapper"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
const value = ref('option2')
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]
</script>Custom Value Field
By default each option's value is read from its value key. Use valueField to read it from a different key — e.g. id — so @update:value returns that field.
<template>
<HLSelect
id="select-value-field"
:options="options"
:value="value"
value-field="id"
placeholder="Select one"
@update:value="val => (value = val)"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
const value = ref(null)
// Options use `id` instead of `value` as the value key
const options = [
{ label: 'Alpha', id: 'a' },
{ label: 'Beta', id: 'b' },
{ label: 'Gamma', id: 'c' },
]
</script>Event Testing
This example logs the events the select emits as you interact with it. Try the following:
- Open and close the dropdown to test
@update:show - Select or clear a value to test
@update:valueand@clear - Type in the search box (
filterable) to test@search - Focus and blur the trigger to test
@focusand@blur
Event Log:
<template>
<HLSelect
id="select-events"
:options="options"
:value="value"
clearable
filterable
@update:value="handleChange"
@update:show="val => addEventLog('@update:show → ' + val)"
@search="val => addEventLog('@search → ' + val)"
@clear="addEventLog('@clear')"
@focus="addEventLog('@focus')"
@blur="addEventLog('@blur')"
/>
<div class="text-sm">
<p class="font-bold mb-2">Event Log:</p>
<div v-if="eventLog.length === 0" class="text-gray-500">No events logged yet. Interact with the select above.</div>
<div v-for="(log, index) in eventLog" :key="index" class="text-gray-700">{{ log.timestamp }}: {{ log.event }}</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { HLSelect } from '@platform-ui/highrise'
const value = ref(null)
const options = [
{ label: 'Option 1', value: 'option1' },
{ label: 'Option 2', value: 'option2' },
{ label: 'Option 3', value: 'option3' },
]
const eventLog = ref<{ event: string; timestamp: string }[]>([])
const addEventLog = (event: string) => {
eventLog.value.unshift({ event, timestamp: new Date().toLocaleTimeString() })
if (eventLog.value.length > 5) {
eventLog.value.pop()
}
}
const handleChange = (val: string | number | null) => {
value.value = val
addEventLog('@update:value → ' + JSON.stringify(val))
}
</script>Design Guidelines
Input components use a box-shadow to render their focus ring. Box-shadows render outside the element's bounds and may be clipped by any ancestor using overflow: hidden (e.g. Tab Panels or Dropdown Menus).
To prevent this, add a small gutter padding to the component's wrapper to ensure there is enough room for the focus ring to render without being cut off.
<div class="p-[3px]">
<!-- Your component here -->
</div>Accessibility
- Tie helper/error copy via
aria-describedby, and expose loading states witharia-busywhen fetching options. - The trigger exposes combobox/listbox semantics (
aria-expanded,aria-controls,aria-haspopup) automatically; passariaLabelorariaLabelledbyso screen readers announce intent consistently.
Imports
import { HLSelect } from '@platform-ui/highrise'Props
| Prop | Type | Default | Description |
|---|---|---|---|
| id | string | Auto (hr-select-*) | Unique identifier for the select. When omitted the component generates an accessible id used for the trigger and dropdown. |
| allowTagTruncation | boolean | false | Enable truncation of tag text in multiple select mode with ellipsis |
| ariaLabel | string | undefined | undefined | ARIA label for accessibility. Provides accessible name for screen readers |
| ariaLabelledby | string | undefined | undefined | ID of element that labels this select. Use when label is provided by another element |
| clearable | boolean | undefined | undefined | Shows a clear button to reset the selection; emits @clear when used |
| disabled | boolean | undefined | undefined | Disables the select |
| filterable | boolean | undefined | undefined | Enables search/filter functionality |
| loading | boolean | undefined | undefined | Shows loading state |
| maxTagCount | number | 'responsive' | 'responsive' | Maximum number of visible tags |
| maxTagWidth | string | number | 136 | Maximum width for truncated tags (in pixels if number, or CSS units if string) |
| menuProps | HTMLAttributes | {} | Additional props to pass to the dropdown menu |
| multiple | boolean | false | Enables multiple selection |
| optionHeight | number | undefined | undefined | Height of each option in pixels |
| optionRenderer | (option: SelectOption) => VNodeChild | undefined | undefined | Custom function to render options |
| options | SelectOption[] | [] | Array of options to display |
| placeholder | string | undefined | undefined | Placeholder text when no selection |
| remote | boolean | false | Enable remote search mode. When true, the component will emit search events instead of filtering options locally. Required for async data fetching scenarios. |
| resetMenuOnOptionsChange | boolean | false | Controls whether the dropdown menu position resets when options array changes. Set to true for dynamic content scenarios where menu positioning needs to update. |
| roundedTags | boolean | false | Use rounded styling for tags |
| show | boolean | undefined | undefined | Controls dropdown visibility |
| showArrow | boolean | undefined | undefined | Show/hide dropdown arrow |
| showAvatarInTags | boolean | true | Show avatar in multiple selection tags |
| showCheckmark | boolean | undefined | undefined | Show/hide option checkmark |
| showSearchIcon | boolean | false | Shows search icon in input |
| size | 'lg' | 'md' | 'sm' | 'xs' | '2xs' | '3xs' | md | Size of the select |
| to | string | boolean | HTMLElement | undefined | undefined | Teleport dropdown to element/disable |
| triggerFontSize | HLTextSize | undefined | Font size for the trigger text |
| triggerFontWeight | HLTextWeight | undefined | Font weight for the trigger text |
| type | 'default' | 'avatar' | 'default' | Visual style variant |
| value | string | string[] | null | undefined | Selected value(s) |
| valueField | string | 'value' | Field name to use as the value in options |
| wrapperClass | string | undefined | Additional CSS class for the wrapper element |
Types
SelectOption
The options prop accepts an array of SelectOption objects.
interface SelectOption {
description?: string // Optional description text for the option
disabled?: boolean // If true: option is not selectable in the dropdown, and when selected its tag is disabled (visual + non-closable)
label?: string // Option label text
/** @deprecated Use tagProps.color instead */
tagColor?: HLTagColor // Color of the tag (when used in tags mode)
src?: string // Source URL (e.g. for avatar images)
tagRenderer?: (() => VNodeChild) | VNodeChild
type: HLSelectOptionType // Type of the option ('group' | 'divider')
value?: string | number // The value of the option
/** Complete tag props object for full control over tag rendering */
tagProps?: SelectTagProps // Advanced tag configuration
}
interface SelectTagProps {
color?: HLTagColor // Tag color
bordered?: boolean // Whether tag has border
closable?: boolean // Whether tag can be closed
disabled?: boolean // Whether tag is disabled
round?: boolean // Whether tag is rounded
count?: number // Count to display in tag
dropdown?: 'open' | 'close' | false // Dropdown state
interactive?: boolean // Whether tag is interactive
truncate?: boolean // Whether to truncate text
maxWidth?: string | number // Maximum width for truncated text
avatarProps?: any // Avatar props (HLAvatarProps)
}Group Option
When type is 'group', the option can include children:
interface SelectGroupOption extends SelectOption {
type: 'group'
label: string // Group header text
key: string // Unique identifier for the group
children: SelectOption[] // Array of child options
}Divider Option
When type is 'divider', it renders a horizontal line separator:
interface SelectDividerOption {
type: 'divider'
}Examples
// Basic option
{
label: "Option 1",
value: "opt1",
description: "Optional description"
}// Group with children
{
type: "group",
label: "Group 1",
key: "group1",
children: [
{ label: "Child 1", value: "child1" },
{ type: "divider" },
{ label: "Child 2", value: "child2" }
]
}// Tag Renderer
{
label: "Option 1",
value: "opt1",
tagRenderer: () => h(HLTag, { color: "blue" }, "Tag Content")
}Emits
| Name | Parameters | Description |
|---|---|---|
@update:value | (value: string | number | string[] | number[] | null, option: SelectOption | SelectOption[] | null) | Triggered when selection changes |
@update:show | (value: boolean) | Triggered when dropdown visibility changes |
@scroll | (e: Event) | Triggered on dropdown scroll |
@search | (value: string) | Triggered when search input changes |
@clear | () | Triggered when selection is cleared |
@focus | () | Triggered when input is focused |
@blur | () | Triggered when input is blurred |
Methods
| Name | Parameters | Description |
|---|---|---|
focusInput | () | Focuses the input field |
blurInput | () | Blurs the input field |
focus | () | Focuses the select |
blur | () | Blurs the select |
Slots
| Name | Parameters | Description |
|---|---|---|
| default | () | The default content slot |
| icon | () | Custom icon slot |
| header | () | Header content for dropdown |
| action | () | Action content for dropdown |
| arrow | () | Custom arrow icon |
| empty | () | Content when no options exist |
| option | { option, selected, disabled, multiple, value } | Custom option renderer slot |
| tag | { option, selected, disabled, multiple, value, handleClose } | Customizes the content rendered inside each selected tag for multiple selection and the selected value for single selection |
| edit-actions | () | Edit actions content for inline mode |