voo-select
voo-select
Vue 3 port of bootstrap-select with zero jQuery.
Disclaimer: This is a personal exploration project. I am not responsible for any malfunction or issues that may arise from its use.
Installation
pnpm add voo-select
Peer dependencies:
vue^3.5.0bootstrap>=5.0.0
Usage
<script setup lang="ts">
import { ref } from "vue";
import { VooSelect, type VooSelectOptionInput } from "voo-select";
import "voo-select/dist/voo-select.css";
const value = ref<string | string[]>();
const options: VooSelectOptionInput[] = [
"Apple",
{ text: "Banana", value: "banana" },
{ text: "Cherry", value: "cherry", selected: true },
];
</script>
<template>
<voo-select
v-model="value"
:options="options"
placeholder="Pick one"
/>
</template>
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options |
VooSelectOptionInput[] |
[] |
Option data |
modelValue |
string | number | (string | number)[] |
— | v-model value; emits the option's original value type |
multiple |
boolean |
false |
Enable multiple selection |
disabled |
boolean |
false |
Disable the select |
size |
'sm' | 'md' | 'lg' |
'md' |
Button size |
menuSize |
'auto' | number | false |
'auto' |
Number of visible items in menu |
placeholder |
string | null |
null |
Placeholder text |
liveSearch |
boolean |
false |
Enable search/filter |
liveSearchPlaceholder |
string | null |
null |
Search input placeholder |
liveSearchStyle |
'contains' | 'startsWith' |
'contains' |
Search matching style |
liveSearchNormalize |
boolean |
false |
Accent-insensitive search |
style |
string |
'btn-light' |
Button style class |
styleBase |
string |
'btn' |
Base button class |
width |
'auto' | 'fit' | string | false |
false |
Select width |
maxOptions |
number | false |
false |
Max selections in multi-select |
loading |
boolean |
false |
Show a loading row in the menu (e.g. lazy fetch on shown) |
loadingText |
string |
'Loading…' |
Text of the loading row |
actionsBox |
boolean |
false |
Show select/deselect all buttons |
doneButton |
boolean |
false |
Show done button |
doneButtonText |
string |
'Close' |
Done button text |
allowClear |
boolean |
false |
Show clear button |
selectedTextFormat |
string |
'values' |
Display format: 'values', 'static', 'count' |
countSelectedText |
function |
— | Format function for count display |
multipleSeparator |
string |
', ' |
Separator for selected items |
selectAllText |
string |
'Select All' |
Select all button text |
deselectAllText |
string |
'Deselect All' |
Deselect all button text |
noneSelectedText |
string |
'Nothing selected' |
Text when nothing selected |
noneResultsText |
string |
'No results matched {0}' |
Text when no search results |
showSubtext |
boolean |
false |
Show subtext in button |
showIcon |
boolean |
true |
Show icons in button |
showContent |
boolean |
true |
Show custom HTML content |
showTick |
boolean |
false |
Checkmark on single-select |
iconBase |
string |
'' |
Icon font base class (e.g. 'fa') |
tickIcon |
string |
'voo-select-ok-default' |
Checkmark icon class |
header |
string | false |
false |
Menu header text |
dropupAuto |
boolean |
true |
Auto-detect dropup direction |
dropdownAlignRight |
boolean | 'auto' |
false |
Right-align dropdown |
container |
string | false |
false |
Teleport the menu to this selector (e.g. 'body'); root classes are copied |
hideDisabled |
boolean |
false |
Hide disabled options |
selectOnTab |
boolean |
false |
Tab selects in dropdown |
virtualScroll |
number | boolean |
600 |
Virtual scroll threshold |
sanitize |
boolean |
true |
Enable HTML sanitization |
whiteList |
object |
— | Allowed HTML tags/attributes |
sanitizeFn |
function | null |
null |
Custom sanitize function |
source |
object |
— | Data source config (fetching/pagination) |
maxOptionsText |
function | string[] |
— | Text when max selections reached |
windowPadding |
number | number[] |
0 |
Window padding for auto-size |
display |
string | false |
false |
Display style for dropdown |
template |
object |
— | Template overrides ({ caret: '<span…>' }) |
title |
string | null |
null |
Button title while nothing is selected |
name |
string |
— | Form field name (see "Forms" below) |
required |
boolean |
false |
Native form validation (see "Forms" below) |
locale |
string |
'en' |
Locale for the default texts |
Text props (noneSelectedText, selectAllText, …) default to the active locale; passing one overrides the locale text.
options format
Accepts plain strings, option objects, or group (optgroup) objects:
// Simple string
"Apple"
// Option object
{
text?: string
value?: string | number
html?: string
selected?: boolean
disabled?: boolean
title?: string
icon?: string
subtext?: string
tokens?: string
className?: string
style?: string
divider?: boolean
}
// Optgroup
{
label: string
disabled?: boolean
className?: string
children?: VooSelectOptionInput[]
options?: VooSelectOptionInput[] // alias for children
}
Slots
Use scoped slots to render Vue components instead of HTML strings. Each slot is optional; omitted slots use the existing HTML/text rendering.
<template lang="pug">
voo-select(v-model="value" :options="options")
template(#option="{ option, selected, active, disabled }")
my-option(:label="option.text" :selected="selected" :active="active" :disabled="disabled")
template(#selected-option="{ option }")
span.badge.text-bg-primary {{ option.text }}
template(#optgroup="{ group }")
strong {{ group.display }}
</template>
| Slot | Props | Replaces |
|---|---|---|
option |
{ option, index, selected, active, disabled } |
Option icon, HTML/text and subtext in the menu |
selected-option |
{ option, index } |
Each selected option's title, icon, HTML/text and subtext in the button |
optgroup |
{ group, index } |
Group label and subtext |
option and group are normalized OptionData records; group.display contains the sanitized group label. Treat slot props as read-only. Menu indexes include group headers/dividers and refer to the filtered list; selected-option indexes refer to the options displayed in the button. The exported VooSelectSlots, OptionSlotProps, SelectedOptionSlotProps and OptgroupSlotProps types describe these contracts.
Slots take precedence over HTML/text and the showIcon, showContent and showSubtext display flags. The button still respects placeholder, static/count formatting, separators and the 50-option display limit. The select keeps the option container, selection tick, keyboard navigation and ARIA state; use presentational content inside options rather than nested buttons, links or inputs.
Keep supplying text, value and optional tokens: search, keyboard typeahead, the button tooltip and native form submission use option data, not the rendered slot content. Vue slot content is rendered normally; the HTML sanitizer still applies to HTML strings, not to your Vue components or any v-html you add yourself.
Virtual lists mount slot components only for the rendered rows and measure their actual heights, including reactive size changes. Offscreen components can unmount; keep persistent state in the parent, keyed by option value.
Forms
With name or required, a hidden native <select> carries the value into <form> submissions (FormData) and native validation (checkValidity()). Only the selected options are mirrored into it, so large lists don't double the DOM size. Without name or required it isn't rendered.
Methods
Exposed through a template ref:
<script setup>
import { ref } from "vue";
const picker = ref();
picker.value?.val(["a", "b"]);
</script>
<template>
<voo-select
ref="picker"
multiple
/>
</template>
| Method | Description |
|---|---|
val(value?) |
Get the current value, or set it |
selectAll() |
Select all visible options (respects limits) |
deselectAll() |
Deselect all visible options |
refresh() |
Rebuild the options from the options prop |
open() |
Open the menu |
close() |
Close the menu |
toggle() |
Toggle the menu |
Events
| Event | Payload | Description |
|---|---|---|
loaded |
— | Fires after initialization |
rendered |
— | Fires after button text is re-rendered |
refreshed |
— | Fires after options are refreshed |
changed |
ChangedArguments |
Fires after value changes |
fetched |
— | Fires after data is fetched |
show |
Event |
Fires when dropdown is shown |
shown |
Event |
Fires after dropdown is shown |
hide |
Event |
Fires when dropdown is hidden |
hidden |
Event |
Fires after dropdown is hidden |
maxReached |
— | Fires when max options is reached |
maxReachedGrp |
— | Fires when optgroup max is reached |
update:modelValue |
value | v-model update |
Styles
import "voo-select/dist/voo-select.css";
Bootstrap CSS must be loaded separately (either via CDN or your own import).
Development
pnpm install
pnpm dev # Demo page with HMR
pnpm typecheck # TypeScript check
pnpm build # Typecheck + build library
pnpm test # Run tests
The main demo includes a Vue slots playground section (/#slots) with reactive Vue components, multiple selection, Teleport, forms, HTML fallbacks and 10,000 virtual options.
Related Packages
Extended multiselect dropdown component for Vue 3 with search, tagging, async op...
Extended multiselect dropdown component for Vue 2 with search, tagging, async op...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.5.1 | ^3.5.0 | MIT | |
| 0.5.0 | ^3.5.0 | MIT | |
| 0.4.5 | ^3.5.0 | MIT | |
| 0.4.4 | ^3.5.0 | MIT | |
| 0.4.3 | ^3.5.0 | MIT | |
| 0.4.2 | ^3.5.0 | MIT | |
| 0.4.1 | ^3.5.0 | MIT | |
| 0.4.0 | ^3.5.0 | MIT | |
| 0.3.2 | ^3.5.0 | MIT | |
| 0.3.1 | ^3.5.0 | MIT | |
| 0.3.0 | ^3.5.0 | MIT | |
| 0.2.2 | ^3.5.0 | MIT |
Pre-releases (1)
| Version | Released | License |
|---|---|---|
| 0.0.0-stage |