CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vue-multiselect

Vue 3 compatible multiselect component with advanced selection, search, tagging, and grouping capabilities

Pending
Overview
Eval results
Files

custom-rendering.mddocs/

Custom Rendering

Comprehensive slot system enabling complete customization of all UI elements including options, tags, labels, and controls.

Capabilities

Basic Slots

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>

Selection Display Slots

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>

Option Display Slots

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>

Advanced Slot Combinations

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>

Slot Prop Reference

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

docs

advanced-configuration.md

basic-selection.md

custom-rendering.md

grouped-options.md

index.md

search-filtering.md

tagging-mode.md

tile.json