Vue 3 Select Component
website·Indexed 29 days ago
https://vue3-select-component.vercel.appAn accessible select component for Vue 3 (3.5+) focusing on developer experience, type safety, and ease of use. It provides both an assembled Select for quick implementation and headless primitives for custom composition. Features include single and multi-select modes, WAI-ARIA Combobox Pattern compliance, TypeScript support, and compatibility with Nuxt 3 and 4.
What's inside Vue 3 Select Component
- The Vue 3 Select Component is a type-safe and accessible select library that offers two ways to implement selection UI: a 'batteries-included' assembled component for quick deployment (supporting v-model, search, clear, and multi-select) or a set of headless primitives for full control over the UX and design system.
Understand the vue3-select-component Primitives architecture
Primitives are headless building blocks that share state via
SelectRootusing provide/inject. All primitives must be nested inside aSelectRootto function. The hierarchy is as follows:- SelectRoot (State + Context Provider)
- SelectTrigger
- SelectIcon
- SelectValue
- SelectInput (used when searchable)
- SelectTrailingIcon
- SelectClear
- SelectPopover
- SelectListbox
- SelectNoOptions
- SelectOption (xN)
- SelectGroup (Optional)
- SelectGroupLabel (Optional)
- SelectSeparator (Optional)
- SelectListbox
- SelectTrigger
- SelectRoot (State + Context Provider)
Understand the assembled Select component architecture
The assembledSelectis a fixed composition of primitives. It usesSelectRootas the state owner, which provides aSelectContextinjected into child primitives. The component tree consists ofSelectRootcontaining aSelectTrigger(with optionalSelectIcon,SelectValue,SelectInput,SelectTrailingIcon, andSelectClear) and aSelectPopovercontaining aSelectListbox(withSelectNoOptions,SelectOptionentries, and an optionalSelectCreateItem).Migrate from vue3-select-component v0 to v1
v1 is a breaking release that introduces a new architecture. While v0 used a single monolithic component, v1 provides an assembledSelectcomponent for common use cases and a set of headless primitives for full customization. v0 prop aliases are not supported in v1; all props must be renamed to their v1 equivalents.Accessibility overview of Vue 3 Select Component
The Vue 3 Select Component follows the WAI-ARIA Combobox Pattern. It provides built-in support for semantic ARIA attributes (combobox,listbox,option), full keyboard navigation, focus management usingaria-activedescendantfor searchable inputs, and screen reader support via descriptive labels and state attributes.Update imports for v1 and Primitives
Update the main component import and add imports for the new primitives if needed. The bundled CSS import remains the same for the assembledSelectcomponent.// v1 Assembled Component import Select from "vue3-select-component" import "vue3-select-component/styles" // v1 Primitives (New) import { SelectRoot, SelectTrigger } from "vue3-select-component/primitives"Search behavior and focus management
Whensearchableis enabled, typing filters options in real time and the active option syncs to the first matching result. Focus remains on the input whilearia-activedescendanttracks the highlighted option. Disabled options are automatically skipped during keyboard navigation.<script setup lang="ts"> import { ref } from "vue" import Select from "vue3-select-component" import "vue3-select-component/styles" const selected = ref<string | null>(null) </script> <template> <Select v-model="selected" :options="[ { label: 'Available', value: '1' }, { label: 'Unavailable', value: '2', disabled: true }, ]" placeholder="Pick an option" /> </template>Use extended properties for custom option rendering
Options can include additional properties beyondlabelandvalue. These properties can be accessed when using the primitive components (likeSelectOptioninsideSelectRoot) to create custom-rendered dropdown items.<script setup lang="ts"> import { SelectRoot, SelectOption, ... } from "vue3-select-component/primitives" const options = [ { label: "JavaScript", value: "js", version: "ES2022", creator: "Brendan Eich" }, ] </script> <template> <SelectRoot v-model="selected" :options="options"> <!-- ... other primitives ... --> <SelectListbox> <SelectOption v-for="option in options" :key="option.value" :value="option.value" :label="option.label" > <span>{{ option.label }} ({{ option.version }})</span> <span>{{ option.creator }}</span> </SelectOption> </SelectListbox> </SelectRoot> </template>Implement infinite scroll for remote options
Infinite scrolling allows loading options in pages as the user scrolls the menu. This is achieved by combining
SelectRootwith a custom list component that utilizes@vueuse/core'suseInfiniteScroll.Implementation strategy:
- State Management: The parent component manages the
optionsarray,isLoadingstate, and thetotalCountof available remote items. - Initial Load: Use the
@menu-openedevent onSelectRootto trigger the first page fetch if the options list is empty. - Appending Data: When the scroll boundary is hit, fetch the next page and append the new options to the existing
:optionsarray passed toSelectRoot. - UI Feedback: Use the
:loadingprop onSelectRootto show a loading indicator during the initial fetch.
pnpm add vue3-select-component @vueuse/core<!-- InfiniteScrollSelect.vue --> <template> <SelectRoot v-model="selected" :options="options" :loading="isLoading" @menu-opened="onMenuOpened" > <SelectTrigger> <SelectValue placeholder="Pick a post" /> <SelectTrailingIcon /> </SelectTrigger> <SelectPopover> <SelectListbox class="infinite-scroll-listbox"> <InfiniteScrollOptionList :loaded-count="options.length" :total-count="totalCount" :is-loading-more="isLoadingMore" :has-more="hasMore" @load-more="loadPosts(true)" /> </SelectListbox> </SelectPopover> </SelectRoot> </template> <style scoped> :deep([data-select-listbox].infinite-scroll-listbox) { overflow: hidden; } </style>- State Management: The parent component manages the
Create new items using the create-item feature
When thecreate-itemprop is enabled, the@createevent is emitted when a user chooses to create a new item from the search query. The payload is the trimmed search query as astring. The component does not automatically updateoptionsorv-model; the developer must handle the event to update the data source.<script setup lang="ts"> import { ref } from "vue" import Select from "vue3-select-component" import "vue3-select-component/styles" const items = ref(["Backlog", "Todo", "Done"]) const value = ref("Backlog") function onCreate(item: string) { items.value.push(item) value.value = item } </script> <template> <Select v-model="value" create-item :options="items" :get-option-label="(item) => item" :get-option-value="(item) => item" placeholder="Pick or create a status" @create="onCreate" /> </template>Implement custom search filtering with filterBy
To filter options based on fields other than the label, pass a custom function to thefilter-byprop ofSelectRoot. This function receives the current option and the search string, returning a boolean to determine if the option should be visible. This is useful for searching across metadata (e.g., searching by both country name and region).<script setup lang="ts"> import { ref } from "vue" import { SelectRoot, SelectTrigger, SelectValue, SelectInput, SelectTrailingIcon, SelectPopover, SelectListbox, SelectNoOptions, SelectOption } from "vue3-select-component/primitives" import "vue3-select-component/styles" type CountryRecord = { name: string; code: string; region: string } const selected = ref<string | null>(null) const countries: CountryRecord[] = [ { name: "France", code: "FR", region: "Europe" }, { name: "Japan", code: "JP", region: "Asia" }, { name: "Brazil", code: "BR", region: "South America" }, { name: "Canada", code: "CA", region: "North America" }, ] function filterByRegion(option: { label: string; value: string }, search: string) { const query = search.trim().toLowerCase() if (!query) return true const record = countries.find((entry) => entry.code === option.value) return option.label.toLowerCase().includes(query) || record?.region.toLowerCase().includes(query) === true } </script> <template> <SelectRoot v-model="selected" searchable :filter-by="filterByRegion"> <SelectTrigger> <SelectValue placeholder="Search by country or region" /> <SelectInput /> <SelectTrailingIcon /> </SelectTrigger> <SelectPopover> <SelectListbox> <SelectNoOptions /> <SelectOption v-for="country in countries" :key="country.code" :value="country.code" :label="country.name"> <span>{{ country.name }}</span> <span>{{ country.region }}</span> </SelectOption> </SelectListbox> </SelectPopover> </SelectRoot> </template>Choose between assembled Select and Primitives
Decide between the high-levelSelectcomponent and the low-level Primitives based on your requirements:Use assembled SelectUse primitives Standard dropdown UX Custom layouts or infinite scroll Quick integration Non-standard trigger or popover behavior Migrating from v0 Full control over every DOM node