Selection
Select
A dropdown for picking one or more values from a list, with search and virtualization.
Install
import { Select } from 'vael-ui'Playground
Fruits
md
chip
bottom
start
import { Select } from 'vael-ui'
<Select placeholder="" :multiple="false" :disabled="false" :invalid="false" size="md" :loading="false" :clearable="false" :maxLabels="0" display="chip" virtualize name="" side="bottom" align="start" :sideOffset="8" :alignOffset="0" closeOnEsc closeOnOutside :forceMount="false" scrollFade motionCss filterPlaceholder="Search..." query="" :items="items">Select</Select>Props
| Name | Type | Default | Description |
|---|---|---|---|
items | readonly T[] | ||
placeholder | string | undefined | ||
multiple | boolean | undefined | false | |
disabled | boolean | undefined | false | |
invalid | boolean | undefined | false | |
size | "md" | "sm" | "lg" | "md" | |
loading | boolean | undefined | false | |
clearable | boolean | undefined | false | |
maxLabels | number | undefined | `multiple` only: how many selected items render as chips before collapsing the rest into a "+N" indicator. Default: uncollapsed. | |
display | "text" | "count" | "chip" | "chip" | `multiple` only: how the trigger renders multiple selections. `'chip'` (default) shows removable chips; `'text'` shows comma-joined labels; `'count'` shows a "N selected" summary. Single-select ignores this prop. |
virtualize | boolean | SelectVirtualizeConfig | undefined | undefined | `true`/`false` forces virtualization on/off; an object also tunes `itemSize`/`overscan`. Default: auto-virtualizes past 100 items. |
name | string | undefined | Renders hidden `<input>`(s) so a plain `<form>` post still carries the selection — repeated `name` when `multiple`. | |
side | "top" | "right" | "bottom" | "left" | "bottom" | Which side of the trigger the panel opens on. |
align | "start" | "end" | "center" | "start" | How the panel aligns against the trigger along that side. |
sideOffset | number | undefined | 8 | Gap between the trigger and the panel, in pixels. |
alignOffset | number | undefined | 0 | Shifts the panel along the alignment axis, in pixels. |
closeOnEsc | boolean | undefined | true | Escape key closes the panel. |
closeOnOutside | boolean | undefined | true | Clicking outside the panel closes it. |
beforeClose | ((done: () => void) => void) | undefined | Custom exit animation; call `done()` when it's complete. Delays the actual close/unmount until then. | |
forceMount | boolean | undefined | false | When true, presence is v-show-driven and owned by the consumer (e.g. AnimatePresence). |
teleportTo | string | HTMLElement | undefined | "body" | CSS selector or an actual DOM element — same contract as Vue's own Teleport `to`. |
scrollFade | boolean | undefined | true | Masks the panel's top/bottom edge as its content scrolls under it, signaling there's more. |
maxPanelHeight | number | undefined | Caps the panel's height at this many pixels even when the viewport has room for more — the option list scrolls internally past it instead of the panel growing indefinitely. Omitted keeps today's behavior (only the viewport limits it). | |
motionCss | boolean | undefined | true | Gates the built-in chip enter/exit/reposition transition (`multiple` + `display="chip"` only). `false` skips it entirely — reach for `@chip-enter`/`@chip-leave` instead if you want a consumer-owned animation (GSAP, motion-v) in its place. |
filter | SelectFilter<T> | undefined | undefined | Shows a built-in search box at the top of the panel. `undefined` (default): no box — most lists are short enough that one is just noise. `true`: box + built-in diacritic/case- insensitive label match against `items`. A function: box + your own sync match against the same `items`. `false`: box, but Select does no matching of its own — pair with `v-model:query` and swap `items` yourself (debounced API search, server-side paging). Virtualization already reacts to whatever `items` ends up being, so a remote result set re-virtualizes for free. |
filterPlaceholder | string | undefined | "Search..." | |
ui | Partial<{ trigger: UiPartValue; value: UiPartValue; positioner: UiPartValue; panel: UiPartValue; header: UiPartValue; filter: UiPartValue; list: UiPartValue; option: UiPartValue; empty: UiPartValue; footer: UiPartValue; }> | undefined | ||
modelValue | string | number | (string | number)[] | null | undefined | null | |
query | string | undefined | "" | |
open | boolean | undefined | false |
Slots
| Name | Type | Description |
|---|---|---|
value | { selected: T | T[] | null; } | |
header | { count: number; total: number; } | Above the filter input (if `filter` is on) or the listbox itself. `count`/`total` are handed through for a result-count readout, but the slot is arbitrary content, not just that. |
filter | { query: string; onKeydown: (event: KeyboardEvent) => void; } | Replaces the built-in filter row entirely — bind your own control straight to `v-model:query` on `<Select>` itself (no need to round-trip through this slot's props for that); `onKeydown` is handed through only so a fully custom input can still opt into arrow/Home/End/Enter listbox navigation the same way the built-in one does. |
filter-icon | any | Swaps just the built-in filter row's leading icon, keeping its `Input` frame. |
item | { item: T; active: boolean; selected: boolean; } | |
empty | any | |
footer | any | Below the listbox — e.g. a "create new" or "view all" action. |
Events
| Name | Type | Description |
|---|---|---|
open-change | [value: boolean, details: PopoverOpenChangeDetails] | |
select | [item: T] | |
change | [value: string | number | (string | number)[] | null] | |
reach-end | [] | |
chip-enter | [el: Element, done: () => void] | |
chip-leave | [el: Element, done: () => void] | |
update:open | [value: boolean] | |
update:modelValue | [value: string | number | (string | number)[] | null] | |
update:query | [value: string] |
Exposed
| Name | Type | Description |
|---|---|---|
triggerEl | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
panelEl | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
positionerEl | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
listEl | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
filterInputRef | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
placement | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
positionerStyle | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
isClosing | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
open | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
close | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
cancelClose | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
activeIndex | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |
scrollToIndex | unknown | Type inference unavailable — vue-component-meta cannot resolve defineExpose on this generic component. |