# Introduction Vue 3 Select Component is an accessible select for Vue 3. It focuses on developer experience, type safety, and ease of use. ## What is Vue 3 Select Component? v1 has two APIs: - **Assembled `Select`**: drop-in component with familiar DX from earlier versions. - **Primitives**: composable building blocks (`SelectRoot`, `SelectTrigger`, `SelectPopover`, and more) for custom UX. Both APIs share the same state machine, accessibility behavior, and TypeScript types. ## What you can do - **Bind data with `v-model`**: single or multi-select with two-way binding - **Search and filter options**: built-in search with custom filter logic - **Customize rendering**: primitive slots for options, values, tags, icons, and empty states - **Style your way**: default assembled styles with `--vs-*` variables, or unstyled primitives with `data-*` hooks - **Teleport the menu**: render the dropdown anywhere in the DOM ## When to use it Use the **assembled `Select`** for a production-ready select with sensible defaults. Good for migrations from `vue-select` or Vue 2. Use **primitives** when you need custom UX like infinite scroll, custom positioning, or non-standard layouts. ::note This documentation covers the v1 API. Still on v0? See the [v0 documentation](https://v0-vue3-select-component.vercel.app/){rel=""nofollow""} . For upgrade steps, see the [migration guide](https://vue3-select-component.vercel.app/guide/migration) . :: # Installation ## Prerequisites [Vue.js](https://vuejs.org){rel=""nofollow""} **3.5+** is required. ## Install the package ::code-group ```bash [pnpm] pnpm add vue3-select-component ``` ```bash [npm] npm install vue3-select-component ``` ```bash [yarn] yarn add vue3-select-component ``` ```bash [bun] bun add vue3-select-component ``` :: ## Import paths v1 exposes two entry points: | Import | Use case | | ---------------------------------- | ---------------------------------------------------- | | `vue3-select-component` | Assembled `Select` component (default export) | | `vue3-select-component/primitives` | Low-level primitive components | | `vue3-select-component/styles` | Default assembled Select styles (`--vs-*` variables) | | `vue3-select-component/styles.css` | Same stylesheet (explicit `.css` subpath) | ```ts // Batteries-included import Select from "vue3-select-component" import "vue3-select-component/styles" // Headless composition import { SelectRoot, SelectTrigger, SelectValue, SelectPopover, SelectListbox, SelectOption, } from "vue3-select-component/primitives" ``` ## Styling Import default styles for the assembled `Select`, then customize via `--vs-*` CSS variables (see [Styling](https://vue3-select-component.vercel.app/guide/styling)). Primitives are headless. Use the default stylesheet or write your own CSS targeting `data-*` hooks. # Usage The assembled `Select` wires up all primitives with sensible defaults. It is the fastest way to get started. ## Use single select Try the assembled `Select` in single-select mode: ::client-only :usage-single-select-demo #fallback :select-demo-fallback :: Import the component and bind `v-model` to your selected value: ```vue [App.vue] ``` Your IDE provides autocompletion and type checking automatically. ## Use multi-select Try multi-select with the `multiple` prop: ::client-only :usage-multi-select-demo #fallback :select-demo-fallback :: Bind `v-model` to an array: ```vue [App.vue] ``` ::note Upgrading from v0? See the [migration guide](https://vue3-select-component.vercel.app/guide/migration) . Props, events, and more have changed. :: ## Disable search or add clear Search is enabled by default on the assembled `Select`. Disable it with `:searchable="false"` when you want a button-only select. Add `clearable` to show a clear button when a value is selected. ::client-only :usage-clearable-demo #fallback :select-demo-fallback :: ```vue [App.vue] ``` ## Bind data with v-model The selected value stays reactive via Vue's `v-model`. | Mode | `v-model` type | Default | | ------ | ------------------------ | ------- | | Single | `string | number | null` | `null` | | Multi | `(string | number)[]` | `[]` | [Learn more about `v-model`](https://vuejs.org/guide/components/v-model.html){rel=""nofollow""}. ## Next steps - [Browse examples](https://vue3-select-component.vercel.app/guide/examples) for clearable, loading, disabled options, and more - [Read the assembled deep dive](https://vue3-select-component.vercel.app/guide/deep-dive) to see how primitives are wired together - [Compose with primitives](https://vue3-select-component.vercel.app/guide/primitives) for full control - [Configure options](https://vue3-select-component.vercel.app/guide/options) with custom shapes and mappers - [Browse the API reference](https://vue3-select-component.vercel.app/api/props) for all props and events # Primitives Primitives are headless building blocks that share state via `SelectRoot`. Each one handles a single concern like trigger, popover, or listbox. ## Understand the architecture ```text SelectRoot (state + context provider) ├── SelectTrigger │ ├── SelectIcon │ ├── SelectValue │ ├── SelectInput (when searchable) │ ├── SelectTrailingIcon │ └── SelectClear └── SelectPopover └── SelectListbox ├── SelectNoOptions └── SelectOption (×N) └── optional: SelectGroup, SelectGroupLabel, SelectSeparator ``` Cross-component state uses provide/inject. Primitives must live inside `SelectRoot`. Group primitives are optional. Use them only when you need labelled sections or separators in the list. ## Build a searchable single select ```vue [CustomSelect.vue] ``` ## Register options two ways **Via `options` prop on `SelectRoot`**: pass a normalized array: ```ts { label: string; value: string | number; disabled?: boolean }[] ``` **Via declarative `SelectOption` children**: register options on mount. Both sources merge. Declarative options are skipped when their `value` already exists from the prop. ## Group options ::note You do not need group primitives for a flat list. Render `SelectOption` children directly inside `SelectListbox` , as in the example above. :: Reach for `SelectGroup`, `SelectGroupLabel`, and `SelectSeparator` only when options belong in labelled sections or need visual dividers. Groups and separators are not selectable and do not change keyboard navigation. ```vue [CustomSelect.vue] ``` Grouped options still register as normal options. When filtering hides every option in a group, the group label is hidden too. ## Teleport the popover `SelectPopover` teleports to `document.body` by default so the dropdown escapes overflow and stacking-context constraints. Scoped styles from parent components still apply to slot content after teleport. Disable teleport or target a custom container with the `teleport` prop: ```vue ``` ## Popover behavior `SelectPopover` wraps reka-ui `Popover`. Pass reka-ui popover props to control placement, collision behavior, modality, and portal deferral: ```vue ``` The assembled `Select` accepts the same props and forwards them to `SelectPopover`. See [Popover props](https://vue3-select-component.vercel.app/api/props#popover-props) for the full list. ## Customize with slots Primitives expose slots for rendering. See [Slots](https://vue3-select-component.vercel.app/api/slots) for the full reference. | Primitive | Slot | Purpose | | -------------------- | ------------------- | ---------------------------------- | | `SelectValue` | `default` | Custom single/multi value display | | `SelectOption` | `default` | Custom option content | | `SelectGroup` | `default` | Group related options | | `SelectGroupLabel` | `default` | Label a group for assistive tech | | `SelectTag` | `default`, `remove` | Custom multi-value tags | | `SelectIcon` | `default` | Leading icon before the value | | `SelectTrailingIcon` | `default` | Custom trailing icon or loading UI | | `SelectNoOptions` | `default` | Custom empty state | ## When to use primitives vs assembled Select | Use assembled `Select` | Use 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 | See the [components reference](https://vue3-select-component.vercel.app/api/components) for props and behavior of each primitive. For how assembled `Select` wires these primitives together, see [Assembled Select deep dive](https://vue3-select-component.vercel.app/guide/deep-dive). # Complex use cases These recipes combine primitives with external libraries or custom list rendering. They assume you are comfortable composing `SelectRoot` yourself. ## Virtualized list Register a large option set on `SelectRoot`, then render only visible rows inside `SelectListbox`. Keyboard navigation and search still run against the full list. This demo registers 50,000 options and mounts roughly a dozen DOM rows at a time with [VueUse `useVirtualList`](https://vueuse.org/core/useVirtualList/){rel=""nofollow""}. ::client-only :example-virtualized-primitives-demo #fallback :select-demo-fallback :: Install [`@vueuse/core`](https://vueuse.org/){rel=""nofollow""} alongside the select package: ::code-group ```bash [pnpm] pnpm add vue3-select-component @vueuse/core ``` ```bash [npm] npm install vue3-select-component @vueuse/core ``` ```bash [yarn] yarn add vue3-select-component @vueuse/core ``` ```bash [bun] bun add vue3-select-component @vueuse/core ``` :: The example splits into two files: - Parent select that registers every option `VirtualizedSelect.vue` - Virtual list child that reads `filteredOptions` from context `VirtualizedOptionList.vue` ::code-group ```vue [VirtualizedSelect.vue] ``` ```vue [VirtualizedOptionList.vue] ``` :: ::note Pass every option to `SelectRoot` through `:options` so filtering and keyboard navigation stay in sync. Set `:teleport="false"` on `SelectPopover` when the virtual list owns scrolling. Keep a fixed row height on each `SelectOption` so `useVirtualList` can measure correctly. Watch `activeOptionValue` and call `scrollTo` so keyboard focus keeps the active row visible. :: ## Infinite scroll Load options in pages as the user scrolls the menu. This demo fetches [JSONPlaceholder](https://jsonplaceholder.typicode.com/guide/){rel=""nofollow""} posts with `_start` and `_limit`, then appends each page to `:options`. ::client-only :example-infinite-scroll-primitives-demo #fallback :select-demo-fallback :: Install [`@vueuse/core`](https://vueuse.org/){rel=""nofollow""} alongside the select package: ::code-group ```bash [pnpm] pnpm add vue3-select-component @vueuse/core ``` ```bash [npm] npm install vue3-select-component @vueuse/core ``` ```bash [yarn] yarn add vue3-select-component @vueuse/core ``` ```bash [bun] bun add vue3-select-component @vueuse/core ``` :: The parent owns fetch state. A child list component attaches [`useInfiniteScroll`](https://vueuse.org/core/useInfiniteScroll/){rel=""nofollow""} to a scroll container inside `SelectListbox`. ::code-group ```vue [InfiniteScrollSelect.vue] ``` ```vue [InfiniteScrollOptionList.vue] ``` ```ts [jsonplaceholder.ts] const POSTS_URL = "https://jsonplaceholder.typicode.com/posts" export async function fetchPostsPage(params: { start: number; limit: number }) { const url = new URL(POSTS_URL) url.searchParams.set("_start", String(params.start)) url.searchParams.set("_limit", String(params.limit)) const response = await fetch(url) const total = Number(response.headers.get("X-Total-Count") ?? 0) const posts = await response.json() return { posts, total } } export function postToSelectOption(post: { id: number; title: string }) { return { label: post.title, value: post.id } } ``` :: ::note Register every loaded page on `:options` so keyboard navigation stays correct. Set the listbox overflow to `hidden` and scroll inside a child container so `useInfiniteScroll` can attach to a ref you control. Use `:loading` for the first fetch and a footer row for later pages. :: ## Remote search Query a remote API when the user types. This demo debounces `@search` and calls JSONPlaceholder with the `title_like` filter param. ::client-only :example-remote-search-primitives-demo #fallback :select-demo-fallback :: ::code-group ```vue [RemoteSearchSelect.vue] ``` ```ts [jsonplaceholder.ts] const POSTS_URL = "https://jsonplaceholder.typicode.com/posts" export async function searchPosts(params: { query: string; limit?: number }) { const url = new URL(POSTS_URL) if (params.query.trim()) { url.searchParams.set("title_like", params.query.trim()) } url.searchParams.set("_limit", String(params.limit ?? 50)) const response = await fetch(url) return response.json() } export function postToSelectOption(post: { id: number; title: string }) { return { label: post.title, value: post.id } } ``` :: ::note Set `:filter-by` to pass through server results. Without it, the built-in label filter hides options that do not match the typed string locally. Track a request id or use `AbortController` to ignore stale responses when search changes quickly. :: ## Next steps - [Primitives guide](https://vue3-select-component.vercel.app/guide/primitives) for the full composition model - [Examples](https://vue3-select-component.vercel.app/guide/examples) for smaller building blocks - [Deep dive](https://vue3-select-component.vercel.app/guide/deep-dive) for how context and option registration work # Nuxt Vue 3 Select Component works in Nuxt 3 and 4. No dedicated module is required. ## Install Install the package as usual. See [Installation](https://vue3-select-component.vercel.app/getting-started/installation). ## Import styles Import the stylesheet once in `nuxt.config.ts`: ```ts [nuxt.config.ts] export default defineNuxtConfig({ css: ["vue3-select-component/styles"], }) ``` ## Use in pages and components Bind `v-model` the same way as in a Vite SPA: ```vue [pages/index.vue] ``` Primitives work the same way. Import from `vue3-select-component/primitives`. ## SSR and SSG caveats The select teleports its dropdown to `document.body` by default. That portal is not present during server render and can cause hydration warnings. Pick one approach: ### `ClientOnly` (recommended) Wrap the component with [`ClientOnly`](https://nuxt.com/docs/4.x/api/components/client-only){rel=""nofollow""} for the simplest fix. The control renders on the client only, once hydration is complete. Add a `#fallback` slot if you want to avoid layout shift. ```vue [pages/index.vue] ``` ::note This is what we use to write interactive docs demos in this project. :: ### Disable teleport Pass `:teleport="false"` to keep the dropdown in the component tree. The closed trigger hydrates without a portal mismatch. Use this when you need the control in static HTML or inside overflow containers you control. ```vue [pages/index.vue] ``` | Attribute | Purpose | | -------------------------- | ------------------------------------------------------- | | `aria-expanded` | Menu open (`true`) or closed (`false`) | | `aria-haspopup="listbox"` | Signals a listbox popup | | `aria-controls` | References the listbox element ID | | `aria-activedescendant` | Points to the keyboard-focused option (only while open) | | `aria-autocomplete="list"` | Suggestions appear in the listbox | ### Non-searchable trigger (`SelectTrigger`) When search is disabled, the trigger renders as a `