Skip to content

@vue-select-plus/core API Reference

The core package ships the headless engine behind <VSelect>. Use it to build a custom select UI while keeping selection, search, keyboard navigation, and tree flattening identical to the Vue component.

Public API

ts
import {
    useSelect,
    useClickOutside,
    type SelectOption,
    type SelectModelValue,
    type SelectValue,
    type FlatOption
} from '@vue-select-plus/core';

Only the four symbols above are part of the stable API. The individual composables (useSelectState, useKeyboard, useOptions, useSelection, useCreator) are internal — they may move or change shape between minor versions.

useSelect

ts
function useSelect(props: {
    options: Ref<ReadonlyArray<SelectOption>>;
    modelValue: Ref<SelectModelValue>;
    multiple: boolean;
    searchable: boolean;
    disabled: Ref<boolean>;
    filterable?: Ref<boolean>;
}): UseSelectReturn;

Returned state

KeyTypePurpose
isOpenRef<boolean>Whether the listbox is open.
searchQueryRef<string>Current search input value.
highlightedIndexRef<number>Index into visibleOptions of the active descendant.
visibleOptionsComputedRef<FlatOption[]>Flattened, filtered, collapse-aware list.
navigableIndicesComputedRef<number[]>Indices of options the keyboard can land on.
collapsedValuesRef<Set<SelectValue>>Tree nodes currently collapsed.
creatorParentValueRef<SelectValue | null>Parent value when creator mode is active.
labelMapComputedRef<Map<SelectValue, string>>Flat value → label lookup (O(1)).

Returned actions

KeyTypePurpose
open / close / toggle() => voidListbox open-state. open() also highlights the selected option.
onKeyDown(e: KeyboardEvent) => voidWire this to your trigger's @keydown.
handleSelect(opt: FlatOption) => voidApply a selection respecting multiple.
isSelected(value?: SelectValue) => booleanSelection predicate.
removeValue(value: SelectValue) => voidRemove one value (multi).
removeLast() => voidPop the last selected value (multi).
clear() => voidReset the model.
toggleCollapse(value: SelectValue) => voidExpand/collapse a tree node.
setHighlight(index: number) => voidMove the active descendant.
startCreator / cancelCreator(value: SelectValue) => void / () => voidOpen/close the inline creator row.

useClickOutside

ts
function useClickOutside(
    targets:
        | MaybeRef<HTMLElement | null>
        | MaybeRef<HTMLElement | null>[],
    handler: (event: PointerEvent | FocusEvent) => void
): void;

Attaches a capture-phase pointerdown listener that fires handler when the event target is outside every target in the array. Pass an array when your dropdown is teleported — include both the anchor and the floating menu so clicks on the menu don't count as "outside".

Types

ts
type SelectValue = string | number;

type SelectModelValue =
    | SelectValue
    | SelectValue[]
    | undefined
    | null;

interface SelectOption {
    value?: SelectValue;          // omit for group-only rows
    label: string;
    disabled?: boolean;
    children?: SelectOption[];    // tree
    group?: string;               // renders as a non-selectable header
}

interface FlatOption extends SelectOption {
    depth: number;
    isGroup: boolean;
    isCreator?: boolean;
    parentValue?: SelectValue;
    key: string | number;
}

Minimal example

vue
<script setup lang="ts">
import { ref, computed } from 'vue';
import { useSelect, useClickOutside, type SelectOption } from '@vue-select-plus/core';

const options = ref<SelectOption[]>([
    { value: 'a', label: 'A' },
    { value: 'b', label: 'B' },
    { value: 'c', label: 'C' }
]);
const modelValue = ref<string | null>(null);
const disabled = ref(false);

const {
    isOpen, visibleOptions, highlightedIndex,
    open, toggle, close, onKeyDown,
    handleSelect, isSelected
} = useSelect({
    options,
    modelValue,
    multiple: false,
    searchable: false,
    disabled
});

const rootRef = ref<HTMLElement | null>(null);
useClickOutside(rootRef, close);
</script>

<template>
    <div ref="rootRef" @keydown="onKeyDown">
        <button
            type="button"
            role="combobox"
            aria-haspopup="listbox"
            :aria-expanded="isOpen"
            @click="toggle"
        >
            {{ modelValue ?? 'Pick…' }}
        </button>

        <ul v-if="isOpen" role="listbox">
            <li
                v-for="(opt, i) in visibleOptions"
                :key="opt.key"
                role="option"
                :aria-selected="isSelected(opt.value)"
                :class="{ active: i === highlightedIndex }"
                @click="handleSelect(opt)"
            >
                {{ opt.label }}
            </li>
        </ul>
    </div>
</template>

This skips ARIA polish like aria-activedescendant, virtualisation, and tag rendering — those are exactly what <VSelect> adds on top.