Everything you wish the HTML select element could do, wrapped up into a lightweight, extensible Vue component.
—
Extensive customization options through slots, component overrides, SCSS variables, and custom positioning for advanced use cases.
Properties that control the visual appearance and behavior of the component.
/**
* Placeholder text displayed when no option is selected
*/
placeholder: String // default: ''
/**
* Sets a Vue transition property on the dropdown menu
* Controls dropdown open/close animation
*/
transition: String // default: 'vs__fade'
/**
* Sets RTL (right-to-left) support
* Accepts 'ltr', 'rtl', or 'auto'
*/
dir: String // default: 'auto'
/**
* Sets the id attribute of the input element
* Useful for accessibility and form integration
*/
inputId: String // default: undefined
/**
* Unique identifier used to generate IDs in HTML
* Must be unique for every instance of vue-select
*/
uid: String | Number // default: uniqueId()Advanced customization through component replacement and positioning.
/**
* Object with custom components to overwrite default implementations
* Keys are merged with defaults, allowing selective overrides
*/
components: Object // default: {}
/**
* Append the dropdown element to the end of the body
* Enables advanced positioning and z-index control
*/
appendToBody: Boolean // default: false
/**
* Custom positioning function when appendToBody is true
* Responsible for positioning the dropdown list dynamically
* @param dropdownList - The dropdown DOM element
* @param component - Vue Select component instance
* @param styles - Calculated position styles
* @returns Cleanup function
*/
calculatePosition: Function // default: built-in positioning logic
/**
* Determines whether the dropdown should be open
* Allows custom dropdown open/close logic
* @param instance - Vue Select component instance
* @returns Whether dropdown should be open
*/
dropdownShouldOpen: Function // default: standard open logic
/**
* Disable the dropdown entirely
* Component becomes a read-only display
*/
noDrop: Boolean // default: falseComprehensive slot-based customization for all UI elements.
/**
* Available scoped slots for complete UI customization
*/
slots: {
/**
* Content displayed before the dropdown toggle area
* @param scope.header - Header-specific data
*/
'header': { scope: { header: Object } },
/**
* Container around each selected option
* @param option - The selected option object
* @param deselect - Function to deselect this option
* @param multiple - Whether multiple selection is enabled
* @param disabled - Whether component is disabled
*/
'selected-option-container': {
option: Object,
deselect: Function,
multiple: Boolean,
disabled: Boolean
},
/**
* Individual selected option display
* @param normalizeOptionForSlot(option) - Normalized option data
*/
'selected-option': { /* normalized option properties */ },
/**
* Custom search input implementation
* @param scope.search - Search-specific data and methods
*/
'search': { scope: { search: Object } },
/**
* Custom dropdown open/close indicator
* @param scope.openIndicator - Indicator-specific data
*/
'open-indicator': { scope: { openIndicator: Object } },
/**
* Custom loading spinner
* @param scope.spinner - Spinner-specific data
*/
'spinner': { scope: { spinner: Object } },
/**
* Content at the top of the dropdown list
* @param scope.listHeader - List header data
*/
'list-header': { scope: { listHeader: Object } },
/**
* Individual dropdown option display
* @param normalizeOptionForSlot(option) - Normalized option data
*/
'option': { /* normalized option properties */ },
/**
* Message displayed when no options are available
* @param scope.noOptions - No options data
*/
'no-options': { scope: { noOptions: Object } },
/**
* Content at the bottom of the dropdown list
* @param scope.listFooter - List footer data
*/
'list-footer': { scope: { listFooter: Object } },
/**
* Content displayed after the dropdown area
* @param scope.footer - Footer-specific data
*/
'footer': { scope: { footer: Object } }
}CSS classes automatically applied based on component state.
computed: {
/**
* Object containing current state CSS classes
* Applied to the root component element
*/
stateClasses: {
'vs--open': Boolean, // Dropdown is open
'vs--single': Boolean, // Single selection mode
'vs--multiple': Boolean, // Multiple selection mode
'vs--searchable': Boolean, // Search is enabled
'vs--unsearchable': Boolean, // Search is disabled
'vs--loading': Boolean, // Loading state active
'vs--disabled': Boolean, // Component is disabled
'vs--rtl': Boolean // Right-to-left text direction
}
}<template>
<v-select
v-model="selected"
:options="options"
placeholder="Custom styled select..."
class="custom-select"
/>
</template>
<script>
export default {
data() {
return {
selected: null,
options: ['Option 1', 'Option 2', 'Option 3']
};
}
};
</script>
<style>
.custom-select .vs__dropdown-toggle {
border: 2px solid #3498db;
border-radius: 8px;
}
.custom-select .vs__selected {
background-color: #3498db;
color: white;
border-radius: 4px;
}
.custom-select .vs__dropdown-menu {
border: 2px solid #3498db;
border-radius: 8px;
}
</style><template>
<v-select
v-model="selectedUser"
:options="users"
label="name"
placeholder="Select user..."
>
<template #selected-option="{ name, email, avatar }">
<div class="user-option">
<img :src="avatar" :alt="name" class="user-avatar" />
<div>
<div class="user-name">{{ name }}</div>
<div class="user-email">{{ email }}</div>
</div>
</div>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selectedUser: null,
users: [
{
name: 'John Doe',
email: 'john@example.com',
avatar: 'https://via.placeholder.com/32'
},
{
name: 'Jane Smith',
email: 'jane@example.com',
avatar: 'https://via.placeholder.com/32'
}
]
};
}
};
</script>
<style scoped>
.user-option {
display: flex;
align-items: center;
gap: 8px;
}
.user-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
}
.user-name {
font-weight: bold;
}
.user-email {
font-size: 0.8em;
color: #666;
}
</style><template>
<v-select
v-model="selectedProduct"
:options="products"
label="name"
placeholder="Select product..."
>
<template #option="{ name, price, image, inStock }">
<div class="product-option" :class="{ 'out-of-stock': !inStock }">
<img :src="image" :alt="name" class="product-image" />
<div class="product-info">
<div class="product-name">{{ name }}</div>
<div class="product-price">${{ price }}</div>
<div v-if="!inStock" class="stock-status">Out of Stock</div>
</div>
</div>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selectedProduct: null,
products: [
{
name: 'Laptop',
price: 999,
image: 'https://via.placeholder.com/40',
inStock: true
},
{
name: 'Phone',
price: 699,
image: 'https://via.placeholder.com/40',
inStock: false
}
]
};
}
};
</script>
<style scoped>
.product-option {
display: flex;
align-items: center;
gap: 10px;
padding: 5px 0;
}
.product-option.out-of-stock {
opacity: 0.6;
}
.product-image {
width: 40px;
height: 40px;
border-radius: 4px;
}
.product-name {
font-weight: bold;
}
.product-price {
color: #2ecc71;
}
.stock-status {
color: #e74c3c;
font-size: 0.8em;
}
</style><template>
<v-select
v-model="selected"
:options="options"
placeholder="Custom indicator..."
>
<template #open-indicator="{ attributes }">
<span v-bind="attributes" class="custom-indicator">
{{ open ? '▲' : '▼' }}
</span>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selected: null,
options: ['Option 1', 'Option 2', 'Option 3']
};
}
};
</script>
<style scoped>
.custom-indicator {
font-size: 12px;
color: #3498db;
transition: transform 0.2s;
}
</style><template>
<v-select
v-model="selected"
:options="options"
placeholder="Custom search..."
>
<template #search="{ attributes, events }">
<input
v-bind="attributes"
v-on="events"
class="custom-search"
placeholder="🔍 Type to search..."
/>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selected: null,
options: ['Apple', 'Banana', 'Cherry', 'Date']
};
}
};
</script>
<style scoped>
.custom-search {
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 8px 12px;
border-radius: 4px;
}
.custom-search::placeholder {
color: rgba(255, 255, 255, 0.7);
}
</style><template>
<v-select
v-model="selected"
:options="options"
:loading="isLoading"
placeholder="Custom loading..."
>
<template #spinner>
<div class="custom-spinner">
<div class="bounce1"></div>
<div class="bounce2"></div>
<div class="bounce3"></div>
</div>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selected: null,
options: [],
isLoading: false
};
},
methods: {
async loadOptions() {
this.isLoading = true;
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 2000));
this.options = ['Loaded Option 1', 'Loaded Option 2'];
this.isLoading = false;
}
},
mounted() {
this.loadOptions();
}
};
</script>
<style scoped>
.custom-spinner {
display: flex;
justify-content: center;
align-items: center;
gap: 2px;
}
.custom-spinner > div {
width: 6px;
height: 6px;
background-color: #3498db;
border-radius: 100%;
animation: sk-bouncedelay 1.4s infinite ease-in-out both;
}
.bounce1 { animation-delay: -0.32s; }
.bounce2 { animation-delay: -0.16s; }
@keyframes sk-bouncedelay {
0%, 80%, 100% { transform: scale(0); }
40% { transform: scale(1.0); }
}
</style><template>
<v-select
v-model="selected"
:options="options"
:components="customComponents"
placeholder="Custom components..."
/>
</template>
<script>
import CustomDeselect from './CustomDeselect.vue';
import CustomOpenIndicator from './CustomOpenIndicator.vue';
export default {
data() {
return {
selected: null,
options: ['Option 1', 'Option 2', 'Option 3'],
customComponents: {
Deselect: CustomDeselect,
OpenIndicator: CustomOpenIndicator
}
};
}
};
</script><template>
<div class="container">
<v-select
v-model="selected"
:options="options"
:appendToBody="true"
:calculatePosition="customPosition"
placeholder="Dropdown appended to body..."
/>
</div>
</template>
<script>
export default {
data() {
return {
selected: null,
options: Array.from({ length: 20 }, (_, i) => `Option ${i + 1}`)
};
},
methods: {
customPosition(dropdownList, component, { width, left, top }) {
// Custom positioning logic
dropdownList.style.position = 'absolute';
dropdownList.style.width = width;
dropdownList.style.left = left;
dropdownList.style.top = top;
dropdownList.style.zIndex = '9999';
// Return cleanup function
return () => {
dropdownList.style.position = '';
dropdownList.style.width = '';
dropdownList.style.left = '';
dropdownList.style.top = '';
dropdownList.style.zIndex = '';
};
}
}
};
</script>
<style scoped>
.container {
height: 200px;
overflow: hidden;
border: 1px solid #ddd;
padding: 20px;
}
</style><template>
<div>
<button @click="toggleDirection">
Toggle Direction (Current: {{ direction }})
</button>
<v-select
v-model="selected"
:options="options"
:dir="direction"
placeholder="RTL/LTR support..."
/>
</div>
</template>
<script>
export default {
data() {
return {
selected: null,
direction: 'ltr',
options: ['خيار 1', 'خيار 2', 'خيار 3'] // Arabic options
};
},
methods: {
toggleDirection() {
this.direction = this.direction === 'ltr' ? 'rtl' : 'ltr';
}
}
};
</script><template>
<v-select
v-model="selectedTeamMember"
:options="teamMembers"
:components="customComponents"
label="name"
placeholder="Select team member..."
class="team-select"
>
<template #header>
<div class="select-header">
<h4>Team Members</h4>
</div>
</template>
<template #selected-option="member">
<div class="selected-member">
<img :src="member.avatar" :alt="member.name" />
<span>{{ member.name }}</span>
<span class="role">{{ member.role }}</span>
</div>
</template>
<template #option="member">
<div class="member-option" :class="{ offline: !member.online }">
<img :src="member.avatar" :alt="member.name" />
<div class="member-details">
<div class="member-name">{{ member.name }}</div>
<div class="member-role">{{ member.role }}</div>
<div class="member-status">
<span class="status-dot" :class="{ online: member.online }"></span>
{{ member.online ? 'Online' : 'Offline' }}
</div>
</div>
</div>
</template>
<template #no-options>
<div class="no-members">
No team members found. Try adjusting your search.
</div>
</template>
<template #footer>
<div class="select-footer">
<small>{{ teamMembers.length }} team members total</small>
</div>
</template>
</v-select>
</template>
<script>
export default {
data() {
return {
selectedTeamMember: null,
teamMembers: [
{
name: 'Alice Johnson',
role: 'Frontend Developer',
avatar: 'https://via.placeholder.com/40',
online: true
},
{
name: 'Bob Smith',
role: 'Backend Developer',
avatar: 'https://via.placeholder.com/40',
online: false
},
{
name: 'Carol Williams',
role: 'UI/UX Designer',
avatar: 'https://via.placeholder.com/40',
online: true
}
],
customComponents: {
// Could include custom Deselect, OpenIndicator, etc.
}
};
}
};
</script>
<style scoped>
.team-select {
max-width: 400px;
}
.select-header {
padding: 10px 15px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
}
.select-header h4 {
margin: 0;
color: #495057;
}
.selected-member {
display: flex;
align-items: center;
gap: 8px;
}
.selected-member img {
width: 24px;
height: 24px;
border-radius: 50%;
}
.selected-member .role {
font-size: 0.8em;
color: #6c757d;
margin-left: auto;
}
.member-option {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
}
.member-option.offline {
opacity: 0.6;
}
.member-option img {
width: 40px;
height: 40px;
border-radius: 50%;
}
.member-details {
flex: 1;
}
.member-name {
font-weight: bold;
color: #212529;
}
.member-role {
font-size: 0.9em;
color: #6c757d;
}
.member-status {
display: flex;
align-items: center;
gap: 4px;
font-size: 0.8em;
color: #6c757d;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background-color: #dc3545;
}
.status-dot.online {
background-color: #28a745;
}
.no-members {
padding: 20px;
text-align: center;
color: #6c757d;
}
.select-footer {
padding: 8px 15px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
text-align: center;
}
</style>Install with Tessl CLI
npx tessl i tessl/npm-vue-select