Skip to content
RTL Support: Work in progress
Accessibility: Partial
Translations: Not Needed
Migration Guide: Work in progress

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.

Vue
html
<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.

Vue
html
<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.

Vue
html
<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.

Bug
Feature
Vue
html
<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.

Add or create tags

Vue
html
<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.

Vue
html
<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

Loading tags...

Vue
html
<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.

Vue
html
<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>

Set show-search to false to hide the search input.

Vue
html
<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.

Vue
html
<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>

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.

Vue
html
<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

Vue
html
<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.

Vue
html
<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>

Use #header and #footer slots to add custom content above and below the options list.

Vue
html
<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.

ts
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.

Vue
html
<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.

Feature
Vue
html
<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.selected reflects the current selection state, so use it to drive highlight/check visuals.
  • Wire @click to 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) when virtual-scroll is 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.
html
<HLAdvancedSelect
  :options="largeOptions"
  virtual-scroll
  :popover-width="320"
/>

Accessibility

  • The dropdown renders with role="listbox" and aria-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-checked reflecting selection state
  • Search input auto-focuses when the dropdown opens

Imports

ts
import { HLAdvancedSelect } from '@platform-ui/highrise'
import type { HLAdvancedSelectOption, HLAdvancedSelectProps } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
valuestring | number | (string | number)[] | nullundefinedSelected value(s). Use with @update:value or v-model:value.
optionsHLAdvancedSelectOption[][]Array of options. Extends HLDropdownOption with children and meta.
multiplebooleanfalseEnable multiple selection with checkboxes.
selectType'default' | 'tags''default''tags' enables tag-based selection with InputTag trigger.
remotebooleanfalseEmit @search instead of filtering locally.
loadingbooleanundefinedShow skeleton loading UI in dropdown.
showSearchbooleantrueShow or hide the search input.
searchPlaceholderstring'Search'Placeholder for the search input.
triggerPlaceholderstring'Search'Placeholder when nothing is selected.
maxAvatarCountnumber5Max avatars in trigger before +N overflow.
virtualScrollbooleanfalseEnable virtual scrolling for large lists. Recommended alongside a fixed popoverWidth for a stable dropdown size.
maxHeightstring'320px'Max height of the dropdown options area.
dockSelectedToTopbooleanfalseSort selected options to top.
defaultTagPropsPartial<HLTagProps>undefinedStyling for tags in the InputTag trigger (e.g. { round: true }).
defaultAvatarPropsPartial<HLAvatarProps>undefinedAvatar props for the trigger (e.g. { size: 'md' }).
showbooleanundefinedControlled popover visibility. Omit for uncontrolled.
placementHLPopoverPlacement'bottom-start'Dropdown placement relative to trigger.
showArrowbooleanfalseShow popover arrow.
tostring | HTMLElement | falseundefinedTeleport target for the dropdown.
disabledbooleanfalseDisable the component.
showAddTagCTAbooleantrueShow "+ tag" button when search has no matches (tags mode).
handleNewOptionRemotebooleanfalseEmit @newTag instead of creating the tag locally.
resetSearchStringbooleanfalseClear search when value changes.
popoverWidthnumber | 'trigger'undefinedFixed width or match trigger width. Tags mode always matches trigger.
popoverContainerClassstringundefinedAdditional CSS class on the dropdown container.

Emits

NameParametersDescription
@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

NamePropsDescription
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

  1. Always use key as the unique identifier for options (inherited from HLDropdownOption)
  2. Set type: 'avatar' on options that should render with an avatar (image or initials fallback)
  3. Use remote mode for server-side filtering — update options with API results on @search
  4. Use virtual-scroll for lists with more than 50 options
  5. Prefer #header and #footer slots for consistent sticky positioning
  6. Use dock-selected-to-top with multiple mode for better UX with many options