# 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]
No results
```
## 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]
FrontendBackend
```
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]
{{ totalCount.toLocaleString() }} options registered.
{{ list.length }} rows mounted in the DOM.
```
::
::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]
Loading more…
{{ loadedCount }} of {{ totalCount }} posts loaded.
```
```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]
{{ isLoading ? "Searching…" : `No posts match "${searchValue}"` }}
```
```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]
```
With primitives, set `teleport` on `SelectPopover`.
### Defer portal
Pass `defer` on `Select` or `SelectPopover` to delay teleport until after mount. Keeps default body teleport without wrapping the whole control.
Learn more about [deferred teleports](https://vuejs.org/guide/built-ins/teleport.html#deferred-teleport){rel=""nofollow""} and [on Reka-UI popover portal docs](https://reka-ui.com/docs/components/popover#portal){rel=""nofollow""}.
## Static generation
`nuxt generate` follows the same rules. Use `ClientOnly`, `:teleport="false"`, or `defer` on every page that renders a select. Search and keyboard behavior run after client hydration.
::note
Form attributes (
`name`
,
`id`
,
`required`
, and ARIA props) pass through to the trigger. They work in SSR markup when the control itself is server-rendered.
::
# Options
The `options` prop populates the dropdown menu.
## Use the basic structure
Each option needs a `label` and a `value`:
```vue [App.vue]
```
| Property | Description |
| -------- | ------------------------------------------------------------ |
| `label` | Text shown in the dropdown and selected value |
| `value` | Data bound to `v-model` when selected (`string` or `number`) |
::note
Import the `SelectOption` type (exported as `SelectOptionData`) for TypeScript:
```ts
import type { SelectOptionData } from "vue3-select-component"
const options: SelectOptionData[] = [
{ label: "JavaScript", value: "js" },
]
```
::
## Disable individual options
Add `disabled: true` to prevent selection:
```vue [App.vue]
```
Disabled options cannot be selected. They are skipped during keyboard navigation and get `aria-disabled`.
## Use custom option shapes
When your data does not match `{ label, value }`, provide mappers:
```vue [App.vue]
```
| Prop | Type | Default |
| ---------------- | ----------------------------- | -------------- |
| `getOptionLabel` | `(option) => string` | `option.label` |
| `getOptionValue` | `(option) => string | number` | `option.value` |
## Customize search filtering
By default, search matches case-insensitive substrings on the option label.
**Assembled `Select`**: filter receives the source option object:
```ts
type GenericFilterByFn = (
option: GenericOption,
label: string,
search: string,
) => boolean
```
```vue [App.vue]
```
**Primitives (`SelectRoot`)**: filter receives the registered option:
```ts
type FilterByFn = (
option: RegisteredOption,
search: string,
) => boolean
```
## Add extended properties
Options can carry extra fields for custom rendering via primitive slots:
```vue [CustomSelect.vue]
```
See [TypeScript](https://vue3-select-component.vercel.app/guide/typescript) for extending option types safely.
# Multi-select
Multi-select lets users pick more than one option. Enable it with the `multiple` prop on assembled `Select` or `SelectRoot`.
## Enable multi-select
```vue [App.vue]
```
## Understand model behavior
| Aspect | Behavior |
| -------------- | ---------------------------------------------- |
| `v-model` type | `(string | number)[]` |
| Default value | `[]` |
| Selecting | Adds value to the array. Menu stays open. |
| Deselecting | Click a selected option to toggle it off |
| Clear button | Resets to `[]`, emits `optionDeselected(null)` |
## Selected option checkmarks
In multi-select mode, selected options in the dropdown show a checkmark on the right. The assembled `Select` and primitive `SelectOption` render this by default. Override it with the `checkmark` slot on `SelectOption`.
```vue [CustomSelect.vue]
```
Style the layout with `data-select-option-label` and `data-select-option-checkmark`.
## Hide selected options
By default, selected options stay visible in the dropdown so users can deselect them. Set `hide-selected` on assembled `Select` to remove them from the list:
```vue [App.vue]
```
On primitives, pass `hide-selected` to `SelectRoot`:
```vue [CustomSelect.vue]
```
## Customize tag remove icon
In multi-select mode, each tag shows a remove button. Override the icon on the assembled `Select` with the `tag-remove` slot:
```vue [App.vue]
```
The slot receives `{ label, value }` for each tag. On primitives, use the `remove` slot on `SelectTag` or forward it from `SelectValue` with `tag-remove`.
## Display selected values
By default, `SelectValue` renders a `SelectTag` for each selection. Customize tags with primitive slots:
```vue [CustomSelect.vue]
{{ label }}
```
## Remove values with keyboard
When the menu is open, searchable, and the search field is empty, **Backspace** removes the last selected tag. This updates `v-model` without emitting `optionDeselected`.
## Combine with search and clear
```vue [App.vue]
```
- **Search** filters visible options without affecting selected values
- **Clear** removes all selections at once
# Styling
## Default styles for assembled Select
The assembled `Select` ships optional default styles. Import them once in your app entry:
```ts
import "vue3-select-component/styles"
```
Primitives remain **unstyled** unless you import the stylesheet and target their `data-*` hooks yourself.
::note
Styles are not auto-injected (CSP-friendly). Import them manually, as in v0.
::
## CSS variables
Default styles use `--vs-*` variables (same prefix as v0). They are defined on `:root`, `[data-assembled-select]`, and `[data-select-popover]` so teleported dropdown panels inherit your theme.
Override variables on the assembled select root:
```vue [App.vue]
```
Because the popover teleports to `document.body`, set popover-specific variables on `:root` or `[data-select-popover]` when needed:
```css
:root {
--vs-menu-box-shadow: 0 8px 24px rgb(15 23 42 / 0.12);
}
```
### Variable reference
```css
:root {
--vs-width: 100%;
--vs-min-height: 38px;
--vs-padding-y: 4px;
--vs-padding-x: 8px;
--vs-padding: var(--vs-padding-y) var(--vs-padding-x);
--vs-border: 1px solid #e4e4e7;
--vs-border-radius: 4px;
--vs-font-size: 16px;
--vs-font-weight: 400;
--vs-font-family: inherit;
--vs-text-color: #18181b;
--vs-line-height: 1.5;
--vs-placeholder-color: #52525b;
--vs-background-color: #fff;
--vs-disabled-background-color: #f4f4f5;
--vs-outline-width: 1px;
--vs-outline-color: #3b82f6;
--vs-menu-offset-top: 8px;
--vs-menu-height: 200px;
--vs-menu-border: var(--vs-border);
--vs-menu-background-color: var(--vs-background-color);
--vs-menu-box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--vs-menu-z-index: 50;
--vs-option-padding: 8px 12px;
--vs-option-hover-background-color: #f0f0f0;
--vs-option-focused-background-color: #dbeafe;
--vs-option-selected-background-color: #93c5fd;
--vs-multi-value-background-color: #f4f4f5;
--vs-multi-value-label-font-size: 12px;
--vs-multi-value-delete-hover-background-color: #ff6467;
--vs-icon-size: 20px;
--vs-trailing-icon-size: 20px;
--vs-spinner-size: 16px;
}
```
See [`src/styles.css`](https://github.com/TotomInc/vue3-select-component/blob/v1-dev/src/styles.css){rel=""nofollow""} for the full list.
## Tailwind CSS
v1 does not include the v0 `classes` prop. For small changes to the assembled
`Select`, add a class to the component and use Tailwind's arbitrary variants to
target its stable data attributes:
```vue [App.vue]
```
The popover is teleported to `document.body` by default, so descendant variants
on the assembled root cannot reach it. Target the global data hook from your CSS
instead:
```css [app.css]
@layer components {
[data-select-popover] {
@apply rounded-xl border-sky-500 shadow-lg;
}
[data-select-option][data-active="true"] {
@apply bg-sky-100;
}
}
```
When every element needs utility classes, compose the unstyled primitives and
put classes directly on them:
```vue [App.vue]
```
Skip the default stylesheet when you want a fully Tailwind-owned design.
## Headless styling with data attributes
Every primitive exposes stable `data-*` hooks for custom CSS:
| Attribute | Element | State |
| --------------------------- | ---------------------- | --------------------------- |
| `data-select-root` | Root wrapper | |
| `data-assembled-select` | Assembled root | |
| `data-select-trigger` | Trigger button | `aria-expanded` |
| `data-select-value` | Value display | `data-empty` |
| `data-select-input` | Search input | |
| `data-select-popover` | Dropdown panel | `aria-hidden` |
| `data-select-listbox` | Listbox container | |
| `data-select-option` | Option row | `data-value`, `data-active` |
| `data-select-tag` | Multi-value tag | |
| `data-select-tag-remove` | Tag remove button | |
| `data-select-clear` | Clear button | |
| `data-select-icon` | Leading icon | |
| `data-select-trailing-icon` | Trailing icon / loader | `data-open`, `data-loading` |
| `data-select-no-options` | Empty state | |
| `data-select-create-item` | Create action | `data-active` |
### Style the trigger
```css
[data-select-trigger] {
display: flex;
align-items: center;
gap: 0.5rem;
width: 100%;
min-height: 2.375rem;
padding: 0.25rem 0.5rem;
border: 1px solid #e4e4e7;
border-radius: 0.375rem;
background: #fff;
cursor: pointer;
}
[data-select-trigger]:disabled {
opacity: 0.5;
cursor: not-allowed;
}
```
### Style options
Use `data-active` for keyboard-highlighted options and `aria-selected` for selected state:
```css
[data-select-option] {
padding: 0.5rem 0.75rem;
cursor: pointer;
}
[data-select-option][data-active="true"] {
background: #dbeafe;
}
[data-select-option][aria-selected="true"] {
background: #93c5fd;
}
[data-select-option][aria-disabled="true"] {
opacity: 0.5;
cursor: not-allowed;
}
```
### Style the popover
`SelectPopover` teleports to `document.body` by default. Use global CSS or unscoped styles with `[data-select-*]` hooks. Vue preserves scoped attribute selectors on teleported slot content, but parent `overflow` / `z-index` no longer constrain the panel.
```css
[data-select-popover] {
margin-top: 0.5rem;
border: 1px solid #e4e4e7;
border-radius: 0.375rem;
background: #fff;
box-shadow: 0 1px 2px rgb(0 0 0 / 0.05);
z-index: 50;
}
```
### Style multi-value tags
```css
[data-select-tag] {
display: inline-flex;
align-items: center;
gap: 0.25rem;
padding: 0.125rem 0.5rem;
border-radius: 0.25rem;
background: #f4f4f5;
font-size: 0.75rem;
}
[data-select-tag-remove] {
border: none;
background: transparent;
cursor: pointer;
line-height: 1;
}
```
### Style the trailing icon
```css
[data-select-trailing-icon][data-open="true"] {
transform: rotate(180deg);
}
[data-select-trailing-icon][data-loading="true"]::after {
content: "";
display: inline-block;
width: 1rem;
height: 1rem;
border: 2px solid currentColor;
border-top-color: transparent;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
```
## Scope styles to a wrapper
Wrap your select in a container class to scope overrides:
```vue [App.vue]
```
For primitives, scope via a parent class or use `:deep()` as needed.
# TypeScript
Vue 3 Select Component is built with TypeScript. Types flow from your option data through `v-model`, props, events, and slots so you get autocompletion and compile-time checks in both the assembled `Select` and primitive APIs.
## Generic components
`Select` and `SelectRoot` use [Vue 3.3 generic components](https://vuejs.org/api/sfc-script-setup.html#generics){rel=""nofollow""}. Two type parameters drive inference:
| Parameter | Default | Role |
| --------------- | ------------------------------- | -------------------------------------------------- |
| `OptionValue` | `string` | The type bound to `v-model` (`string` or `number`) |
| `GenericOption` | `SelectOptionData` | Your source option objects |
```ts
// Assembled Select: OptionValue first, then GenericOption
Select
// Primitives: OptionValue only
SelectRoot
```
TypeScript usually infers both parameters from `:options` and `v-model`. Pass them explicitly when wrapping the component or when inference is ambiguous.
## Use the SelectOptionData type
The canonical option shape is exported as `SelectOptionData` (alias of internal `SelectOption`):
```ts
import type { SelectOptionData } from "vue3-select-component"
type SelectOptionData = {
label: string
value: Value
disabled?: boolean
}
```
::note
v0 exported this type as
`Option`
. In v1, import
`SelectOptionData`
instead. See the
[migration guide](https://vue3-select-component.vercel.app/guide/migration#update-typescript-imports)
.
::
## Configure value types
`option.value` and `v-model` share the same type. Use `string` or `number`:
```vue [App.vue]
```
## Extend option properties
Add custom fields and keep them typed in mappers, filters, and events:
```vue [App.vue]
```
With primitives, the same `UserOption` type flows into `SelectOption` slot props and `@option-selected` payloads.
## Validate v-model types
The component enforces consistency between `option.value` and `v-model`:
```vue
```
## Use SelectModelValue
The internal `v-model` type is `SelectModelValue`:
```ts
import type { SelectModelValue } from "vue3-select-component"
type SelectModelValue = Value | Value[] | null
```
In practice, bind a narrower type for each mode:
| Mode | Bind as |
| ------------- | ------------------------- |
| Single select | `ref(null)` |
| Multi-select | `ref([])` |
```vue [App.vue]
```
## Type multi-select refs
When wrapping `Select`, pass both generics so events and options stay typed:
```vue [App.vue]
```
## Custom value mapping
When options do not follow `{ label, value }`, use `getOptionLabel` and `getOptionValue`. TypeScript infers `GenericOption` from your array:
```vue [App.vue]
```
::warning
Custom mappers weaken the link between your array type and
`SelectOptionData`
. Prefer typed source objects and mappers over casting options to
`SelectOptionData[]`
. See
[Options](https://vue3-select-component.vercel.app/guide/options#use-custom-option-shapes)
for runtime behavior.
::
If inference fails, set generics explicitly:
```vue