Vue 3 compatible multiselect component with advanced selection, search, tagging, and grouping capabilities
—
Comprehensive slot system enabling complete customization of all UI elements including options, tags, labels, and controls.
Essential slots for customizing core component elements.
/**
* Basic customization slots
*/
interface BasicCustomizationSlots {
/** Custom dropdown caret/arrow icon */
caret: { toggle: () => void };
/** Custom clear button */
clear: { search: string };
/** Custom placeholder content */
placeholder: {};
/** Custom loading indicator */
loading: {};
}Usage Example:
<template>
<VueMultiselect
v-model="selectedOption"
:options="options"
:loading="isLoading"
placeholder="Custom styled select">
<template #caret="{ toggle }">
<button @click="toggle" class="custom-caret">
<i class="icon-chevron-down"></i>
</button>
</template>
<template #clear="{ search }">
<button v-if="search" @click="clearSearch" class="custom-clear">
<i class="icon-x"></i>
</button>
</template>
<template #placeholder>
<span class="custom-placeholder">
<i class="icon-search"></i>
Choose an option...
</span>
</template>
<template #loading>
<div class="custom-loading">
<div class="spinner"></div>
<span>Loading options...</span>
</div>
</template>
</VueMultiselect>
</template>
<style>
.custom-caret {
background: none;
border: none;
padding: 8px;
cursor: pointer;
}
.custom-clear {
background: #ff4757;
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
}
.custom-placeholder {
display: flex;
align-items: center;
gap: 8px;
color: #999;
}
.custom-loading {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
}
.spinner {
width: 16px;
height: 16px;
border: 2px solid #f3f3f3;
border-top: 2px solid #3498db;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>Customize how selected values are displayed.
/**
* Selection display customization slots
*/
interface SelectionDisplaySlots {
/**
* Complete selection area customization
* Replaces default tag display and single value display
*/
selection: {
search: string;
remove: (option: any) => void;
values: any[];
isOpen: boolean;
};
/** Custom individual tag rendering for multiple selection */
tag: {
option: any;
search: string;
remove: (option: any) => void;
};
/** Custom single value label for single selection */
singleLabel: {
option: any;
};
/** Custom text for selection limits */
limit: {};
}Usage Example:
<template>
<VueMultiselect
v-model="selectedUsers"
:options="users"
:multiple="true"
:limit="3"
label="name"
track-by="id">
<template #selection="{ values, remove, isOpen }">
<div class="custom-selection">
<div class="selected-count">
{{ values.length }} user{{ values.length === 1 ? '' : 's' }} selected
</div>
<div v-if="isOpen" class="selection-details">
<div v-for="user in values" :key="user.id" class="selected-user">
<img :src="user.avatar" :alt="user.name" class="user-avatar">
<span>{{ user.name }}</span>
<button @click="remove(user)" class="remove-user">×</button>
</div>
</div>
</div>
</template>
<template #tag="{ option, remove }">
<div class="user-tag">
<img :src="option.avatar" :alt="option.name" class="tag-avatar">
<span class="tag-name">{{ option.name }}</span>
<span class="tag-role">{{ option.role }}</span>
<button @click="remove(option)" class="tag-remove">×</button>
</div>
</template>
<template #singleLabel="{ option }">
<div class="single-user-label">
<img :src="option.avatar" :alt="option.name" class="single-avatar">
<div class="single-user-info">
<div class="single-user-name">{{ option.name }}</div>
<div class="single-user-role">{{ option.role }}</div>
</div>
</div>
</template>
<template #limit>
<span class="custom-limit-text">
<i class="icon-more"></i>
and more...
</span>
</template>
</VueMultiselect>
</template>
<style>
.custom-selection {
padding: 8px;
background: #f8f9fa;
border-radius: 4px;
}
.selected-count {
font-weight: 500;
color: #495057;
}
.selection-details {
margin-top: 8px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.selected-user {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: white;
border-radius: 16px;
font-size: 12px;
}
.user-avatar, .tag-avatar, .single-avatar {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.user-tag {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 8px;
background: #e3f2fd;
border-radius: 16px;
font-size: 12px;
}
.tag-role {
color: #666;
font-size: 10px;
}
.single-user-label {
display: flex;
align-items: center;
gap: 8px;
}
.single-user-info {
display: flex;
flex-direction: column;
}
.single-user-name {
font-weight: 500;
}
.single-user-role {
font-size: 12px;
color: #666;
}
</style>Customize how options appear in the dropdown.
/**
* Option display customization slots
*/
interface OptionDisplaySlots {
/** Custom option rendering */
option: {
option: any;
search: string;
index: number;
};
/** Content before the options list */
beforeList: {};
/** Content after the options list */
afterList: {};
/** Custom message when no options available */
noOptions: {};
/** Custom message when search yields no results */
noResult: {
search: string;
};
/** Custom message when maximum selections reached */
maxElements: {};
}Usage Example:
<template>
<VueMultiselect
v-model="selectedProduct"
:options="products"
:searchable="true"
label="name"
track-by="id">
<template #beforeList>
<div class="product-header">
<h4>Available Products</h4>
<div class="product-stats">
{{ products.length }} products available
</div>
</div>
</template>
<template #option="{ option, search, index }">
<div class="product-option" :class="{ 'featured': option.featured }">
<img :src="option.image" :alt="option.name" class="product-image">
<div class="product-info">
<div class="product-name">
<highlightText :text="option.name" :query="search" />
</div>
<div class="product-price">${{ option.price }}</div>
<div class="product-category">{{ option.category }}</div>
<div class="product-rating">
<span class="stars">{{ '★'.repeat(option.rating) }}</span>
<span class="rating-text">({{ option.reviews }} reviews)</span>
</div>
</div>
<div class="product-actions">
<span v-if="option.inStock" class="stock-status in-stock">In Stock</span>
<span v-else class="stock-status out-of-stock">Out of Stock</span>
<button v-if="option.featured" class="featured-badge">Featured</button>
</div>
</div>
</template>
<template #afterList>
<div class="product-footer">
<a href="/products" class="view-all-link">
View all products →
</a>
</div>
</template>
<template #noOptions>
<div class="no-products">
<i class="icon-package"></i>
<h3>No products available</h3>
<p>Please check back later or contact support.</p>
<button @click="refreshProducts">Refresh</button>
</div>
</template>
<template #noResult="{ search }">
<div class="no-search-results">
<i class="icon-search"></i>
<h3>No products found</h3>
<p>No products match "{{ search }}"</p>
<div class="search-suggestions">
<p>Try searching for:</p>
<button
v-for="suggestion in searchSuggestions"
:key="suggestion"
@click="searchForSuggestion(suggestion)"
class="suggestion-button">
{{ suggestion }}
</button>
</div>
</div>
</template>
<template #maxElements>
<div class="max-selection-message">
<i class="icon-warning"></i>
<p>Maximum number of products selected</p>
<p>Remove a product to select another</p>
</div>
</template>
</VueMultiselect>
</template>
<script>
export default {
data() {
return {
selectedProduct: null,
products: [
{
id: 1,
name: 'Premium Laptop',
price: 1299,
category: 'Electronics',
image: '/images/laptop.jpg',
rating: 5,
reviews: 124,
inStock: true,
featured: true
}
],
searchSuggestions: ['laptop', 'phone', 'tablet', 'headphones']
}
},
methods: {
refreshProducts() {
// Refresh products logic
},
searchForSuggestion(suggestion) {
// Set search to suggestion
this.$refs.multiselect.updateSearch(suggestion);
}
}
}
</script>
<style>
.product-header {
padding: 12px;
border-bottom: 1px solid #e9ecef;
background: #f8f9fa;
}
.product-stats {
font-size: 12px;
color: #666;
}
.product-option {
display: flex;
align-items: center;
padding: 12px;
gap: 12px;
border-bottom: 1px solid #f0f0f0;
}
.product-option.featured {
background: linear-gradient(90deg, #fff3cd 0%, #ffffff 100%);
}
.product-image {
width: 48px;
height: 48px;
object-fit: cover;
border-radius: 4px;
}
.product-info {
flex: 1;
}
.product-name {
font-weight: 500;
margin-bottom: 4px;
}
.product-price {
font-size: 14px;
color: #28a745;
font-weight: 600;
}
.product-category {
font-size: 12px;
color: #666;
}
.product-rating {
font-size: 12px;
margin-top: 4px;
}
.stars {
color: #ffc107;
}
.rating-text {
color: #666;
margin-left: 4px;
}
.product-actions {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.stock-status {
font-size: 11px;
padding: 2px 6px;
border-radius: 10px;
}
.stock-status.in-stock {
background: #d4edda;
color: #155724;
}
.stock-status.out-of-stock {
background: #f8d7da;
color: #721c24;
}
.featured-badge {
background: #ff6b6b;
color: white;
border: none;
padding: 2px 6px;
border-radius: 10px;
font-size: 10px;
}
.product-footer {
padding: 12px;
text-align: center;
border-top: 1px solid #e9ecef;
background: #f8f9fa;
}
.view-all-link {
color: #007bff;
text-decoration: none;
font-size: 14px;
}
.no-products, .no-search-results, .max-selection-message {
padding: 24px;
text-align: center;
}
.search-suggestions {
margin-top: 16px;
}
.suggestion-button {
margin: 4px;
padding: 4px 8px;
border: 1px solid #ddd;
background: white;
border-radius: 16px;
cursor: pointer;
font-size: 12px;
}
</style>Combine multiple slots for complex custom layouts.
<template>
<VueMultiselect
v-model="selectedTeamMembers"
:options="teamMembers"
:multiple="true"
:searchable="true"
label="name"
track-by="id"
class="team-selector">
<!-- Custom header with stats -->
<template #beforeList>
<div class="team-stats-header">
<div class="stat">
<span class="stat-number">{{ teamMembers.length }}</span>
<span class="stat-label">Available</span>
</div>
<div class="stat">
<span class="stat-number">{{ selectedTeamMembers.length }}</span>
<span class="stat-label">Selected</span>
</div>
<div class="stat">
<span class="stat-number">{{ onlineMembers }}</span>
<span class="stat-label">Online</span>
</div>
</div>
</template>
<!-- Custom option with rich member info -->
<template #option="{ option, search }">
<div class="member-option">
<div class="member-avatar-container">
<img :src="option.avatar" :alt="option.name" class="member-avatar">
<div class="status-indicator" :class="option.status"></div>
</div>
<div class="member-details">
<div class="member-name">
<highlightText :text="option.name" :query="search" />
</div>
<div class="member-role">{{ option.role }}</div>
<div class="member-department">{{ option.department }}</div>
</div>
<div class="member-metrics">
<div class="metric">
<span class="metric-value">{{ option.tasksCount }}</span>
<span class="metric-label">Tasks</span>
</div>
<div class="metric">
<span class="metric-value">{{ option.availability }}%</span>
<span class="metric-label">Available</span>
</div>
</div>
<div class="member-actions">
<button @click.stop="viewProfile(option)" class="action-btn">
<i class="icon-user"></i>
</button>
<button @click.stop="sendMessage(option)" class="action-btn">
<i class="icon-message"></i>
</button>
</div>
</div>
</template>
<!-- Custom selection display with team composition -->
<template #selection="{ values, remove, isOpen }">
<div class="team-composition">
<!-- Role distribution chart -->
<div class="role-distribution">
<div
v-for="role in roleDistribution"
:key="role.name"
class="role-segment"
:style="{ width: role.percentage + '%', backgroundColor: role.color }">
</div>
</div>
<!-- Selected members preview -->
<div class="selected-members-preview">
<div
v-for="member in values.slice(0, 5)"
:key="member.id"
class="member-preview">
<img :src="member.avatar" :alt="member.name" class="preview-avatar">
</div>
<div v-if="values.length > 5" class="more-members">
+{{ values.length - 5 }}
</div>
</div>
<!-- Team stats -->
<div class="team-summary">
<span class="team-size">{{ values.length }} members</span>
<span class="team-capacity">{{ totalCapacity }}% capacity</span>
</div>
<!-- Expandable details when open -->
<div v-if="isOpen" class="expanded-selection">
<div v-for="member in values" :key="member.id" class="selected-member-detail">
<img :src="member.avatar" :alt="member.name">
<div class="member-info">
<span class="name">{{ member.name }}</span>
<span class="role">{{ member.role }}</span>
</div>
<button @click="remove(member)" class="remove-member">×</button>
</div>
</div>
</div>
</template>
<!-- Custom footer with team actions -->
<template #afterList>
<div class="team-actions-footer">
<button @click="selectByRole('developer')" class="role-select-btn">
Select All Developers
</button>
<button @click="selectByRole('designer')" class="role-select-btn">
Select All Designers
</button>
<button @click="selectOnlineMembers" class="role-select-btn">
Select Online Members
</button>
</div>
</template>
</VueMultiselect>
</template>
<script>
export default {
computed: {
onlineMembers() {
return this.teamMembers.filter(member => member.status === 'online').length;
},
roleDistribution() {
const roles = {};
this.selectedTeamMembers.forEach(member => {
roles[member.role] = (roles[member.role] || 0) + 1;
});
const total = this.selectedTeamMembers.length;
const colors = {
'developer': '#4CAF50',
'designer': '#2196F3',
'manager': '#FF9800',
'analyst': '#9C27B0'
};
return Object.entries(roles).map(([role, count]) => ({
name: role,
count,
percentage: (count / total) * 100,
color: colors[role] || '#999'
}));
},
totalCapacity() {
return this.selectedTeamMembers
.reduce((total, member) => total + member.availability, 0);
}
},
methods: {
viewProfile(member) {
// Navigate to member profile
},
sendMessage(member) {
// Open messaging interface
},
selectByRole(role) {
const roleMembers = this.teamMembers.filter(member => member.role === role);
this.selectedTeamMembers = [...this.selectedTeamMembers, ...roleMembers];
},
selectOnlineMembers() {
const onlineMembers = this.teamMembers.filter(member => member.status === 'online');
this.selectedTeamMembers = onlineMembers;
}
}
}
</script>Complete reference of all available slot props.
/**
* Complete slot prop interfaces
*/
interface AllSlots {
// Control slots
caret: { toggle: () => void };
clear: { search: string };
// Selection slots
selection: {
search: string;
remove: (option: any) => void;
values: any[];
isOpen: boolean
};
tag: { option: any; search: string; remove: (option: any) => void };
singleLabel: { option: any };
limit: {};
// Content slots
placeholder: {};
loading: {};
// List slots
beforeList: {};
afterList: {};
option: { option: any; search: string; index: number };
// Message slots
noOptions: {};
noResult: { search: string };
maxElements: {};
}Install with Tessl CLI
npx tessl i tessl/npm-vue-multiselect