Skip to content

Composable API ​

The useSelect composable provides full select functionality without any components. You get reactive state and prop getter functions that return the correct attributes for your own HTML elements.

For a component-based alternative, see the Components API.

Complete Example ​

useSelect ​

ts
function useSelect<T>(options?: UseSelectOptions<T>): UseSelectReturn<T>

Creates a fully functional select instance. Returns reactive state, methods, and prop getter functions.

Options ​

ts
interface UseSelectOptions<T> {
  id?: string
  value?: Ref<T | T[] | null | undefined>
  defaultValue?: T | T[] | null
  onValueChange?: (value: T | T[] | null) => void
  open?: Ref<boolean | undefined>
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
  filter?: FilterFn<T>
  debounce?: number
  loop?: boolean
  multiple?: boolean
  max?: number | Ref<number | undefined>
  hideSelected?: boolean | Ref<boolean>
  disabled?: Ref<boolean>
  selectOnTab?: boolean
  placeholder?: Ref<string | undefined>
  items?: Ref<T[] | undefined> | T[]
  labelKey?: keyof T
  valueKey?: keyof T
  resolveLabel?: SelectLabelResolver<T>
}
OptionTypeDefaultDescription
idstringauto-generatedBase ID for ARIA attributes.
valueRefundefinedControlled value ref. When provided, the composable does not manage its own value state.
defaultValueT | T[] | nullnullInitial value for uncontrolled usage.
onValueChange(value) => voidundefinedCallback when value changes. Works with both controlled and uncontrolled modes.
openRef<boolean>undefinedControlled open state ref.
defaultOpenbooleanfalseInitial open state for uncontrolled usage.
onOpenChange(open) => voidundefinedCallback when open state changes.
filterFilterFncase-insensitive label matchCustom filter function. Receives item and query string, returns boolean.
debouncenumberundefinedDebounce delay in ms for the filter function.
loopbooleanundefinedWhether keyboard navigation wraps around.
multiplebooleanfalseEnable multi-select mode.
maxnumber | Ref<number>undefinedMaximum selections in multi-select mode.
hideSelectedboolean | Ref<boolean>falseHide selected items from the dropdown in multi-select mode.
disabledRef<boolean>ref(false)Disables all interaction.
selectOnTabbooleanfalseTab key selects the highlighted option before closing.
placeholderRef<string>undefinedPlaceholder text for the input.
itemsRef<T[]> | T[]undefinedData source for options. Used by resolveLabel for label lookup.
labelKeykeyof TundefinedObject field to use as display label.
valueKeykeyof TundefinedObject field to use as the selected value.
resolveLabel(value: T) => stringundefinedCustom label resolver for values without mounted options.

Return Value ​

ts
interface UseSelectReturn<T> {
  // Prop getters
  getRootProps: (userProps?) => Record<string, unknown>
  getInputProps: (userProps?) => Record<string, unknown>
  getListboxProps: (userProps?) => Record<string, unknown>
  getOptionProps: (item: CollectionItem<T>, userProps?) => Record<string, unknown>

  // Reactive state
  value: Ref<T | T[] | null>
  isOpen: Ref<boolean>
  query: Ref<string>
  items: Readonly<Ref<readonly CollectionItem<T>[]>>
  orderedItems: Ref<CollectionItem<T>[]>
  filteredItems: Ref<CollectionItem<T>[]>
  visibleItems: Ref<CollectionItem<T>[]>
  activeId: Ref<string | null>
  activeIndex: Ref<number>
  multiple: boolean
  isAtMax: Ref<boolean>
  disabled: Ref<boolean>
  placeholder: Ref<string | undefined>

  // Methods
  open: () => void
  close: () => void
  toggle: () => void
  dismiss: () => void
  clear: () => void
  focus: () => void
  removeLast: () => void
  isSelected: (item: CollectionItem<T>) => boolean

  // Collection management
  registerItem: (item: CollectionItem<T>) => void
  unregisterItem: (id: string) => void
  updateItem: (id: string, patch: Partial<CollectionItem<T>>) => void
  resolveLabel: (value: unknown) => string | undefined
  getItemLabel: (item: T) => string
  getItemValue: (item: T) => unknown

  // Element refs
  controlRef: Ref<HTMLElement | null>
  inputRef: Ref<HTMLElement | null>
}

Prop Getters ​

Prop getters are functions that return the correct HTML attributes for each element. They handle ARIA attributes, event handlers, keyboard navigation, and state management. Spread them onto your elements:

vue
<template>
  <div v-bind="getRootProps()">
    <input v-bind="getInputProps()" />
    <ul v-if="isOpen" v-bind="getListboxProps()">
      <li
        v-for="item in visibleItems"
        :key="item.id"
        v-bind="getOptionProps(item)"
      >
        {{ item.label }}
      </li>
    </ul>
  </div>
</template>

getRootProps ​

Returns attributes for the root container element: id, data-state ('open' or 'closed'), and data-disabled.

getInputProps ​

Returns attributes for the input element: role, aria-expanded, aria-activedescendant, aria-autocomplete, value, placeholder, disabled, plus event handlers for input, keydown, mousedown, focusout, compositionstart, and compositionend.

getListboxProps ​

Returns attributes for the listbox container: role="listbox", id, aria-multiselectable (when multiple is true).

getOptionProps ​

Returns attributes for each option: role="option", id, aria-selected, aria-disabled, plus data-selected, data-highlighted, data-disabled attributes, and event handlers for click, mousedown, and mousemove.

Signature:

ts
getOptionProps(item: CollectionItem<T>, userProps?: Record<string, unknown>)

Merging User Props ​

All prop getters accept an optional userProps argument. Your props are merged with the internal props, and event handlers are combined (both run):

vue
<input v-bind="getInputProps({ class: 'my-input', onFocus: handleFocus })" />

Reactive State ​

PropertyTypeDescription
valueRefCurrent selected value
isOpenRef<boolean>Whether the dropdown is open
queryRef<string>Current search query text
itemsRef<CollectionItem[]>All registered items
orderedItemsRef<CollectionItem[]>Items in DOM order
filteredItemsRef<CollectionItem[]>Items after filter is applied
visibleItemsRef<CollectionItem[]>Items visible in dropdown (filtered + hideSelected)
activeIdRef<string | null>ID of the currently highlighted option
activeIndexRef<number>Index of the currently highlighted option
multiplebooleanWhether multi-select is active
isAtMaxRef<boolean>Whether maximum selections have been reached
disabledRef<boolean>Whether the select is disabled
placeholderRef<string>Placeholder text

Methods ​

MethodSignatureDescription
open() => voidOpens the dropdown (no-op if disabled)
close() => voidCloses the dropdown
toggle() => voidToggles the dropdown open/closed
dismiss() => voidCloses the dropdown and restores the query to the selected label (single) or clears it (multi)
clear() => voidClears the selection and query
focus() => voidFocuses the input element
removeLast() => voidRemoves the last selected item (multi-select only, when query is empty)
isSelected(item) => booleanChecks if an item is currently selected

Collection Management ​

When using the composable, you must register and unregister items manually. This is typically done in onMounted and onBeforeUnmount:

ts
import { onMounted, onBeforeUnmount } from 'vue'

const { registerItem, unregisterItem, visibleItems } = useSelect({ ... })

// Register items from your data
const items = ['Apple', 'Banana', 'Cherry'].map((fruit, i) => ({
  id: `option-${i}`,
  value: fruit,
  label: fruit,
  disabled: false,
  element: null,
}))

onMounted(() => items.forEach(registerItem))
onBeforeUnmount(() => items.forEach(item => unregisterItem(item.id)))

TypeScript Types ​

ts
type FilterFn<T> = (item: CollectionItem<T>, query: string) => boolean

interface CollectionItem<T> {
  id: string
  value: T
  label: string
  disabled: boolean
  element: HTMLElement | null
}

type SelectLabelResolver<T> = (value: T) => string | undefined

type SelectValue<T> = T | T[] | null

Usage with Options API ​

The composable works in the Options API via the setup() function:

vue
<script setup lang="ts">
import { ref, onMounted, onBeforeUnmount } from 'vue'
import { useSelect } from 'vue-superselect'

const fruits = ['Apple', 'Banana', 'Cherry']
const { getRootProps, getInputProps, getListboxProps, getOptionProps,
        isOpen, visibleItems, registerItem, unregisterItem } = useSelect()

const items = fruits.map((fruit, i) => ({
  id: `fruit-${i}`, value: fruit, label: fruit, disabled: false, element: null,
}))

onMounted(() => items.forEach(registerItem))
onBeforeUnmount(() => items.forEach(item => unregisterItem(item.id)))
</script>