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>