CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-vue-select

Everything you wish the HTML select element could do, wrapped up into a lightweight, extensible Vue component.

Pending
Overview
Eval results
Files

customization.mddocs/

Customization and Styling

Extensive customization options through slots, component overrides, SCSS variables, and custom positioning for advanced use cases.

Capabilities

Display and Appearance

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()

Component Override System

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: false

Slot System

Comprehensive 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 } }
}

State Classes

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
  }
}

Usage Examples

Basic Styling Customization

<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>

Custom Selected Option Display

<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>

Custom Dropdown Options

<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>

Custom Open Indicator

<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>

Custom Search Input

<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>

Custom Loading Spinner

<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>

Component Override System

<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>

Append to Body with Custom Positioning

<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>

RTL (Right-to-Left) Support

<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>

Complete Customization Example

<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

docs

ajax-loading.md

customization.md

index.md

keyboard-navigation.md

search-filtering.md

selection.md

tagging.md

tile.json