Vue 3 compatible multiselect component with advanced selection, search, tagging, and grouping capabilities
—
Advanced tagging functionality allowing users to create new options dynamically through text input.
Enable tagging mode to allow users to create new options by typing and pressing Enter.
/**
* Basic tagging configuration props
*/
interface BasicTaggingProps {
/** Enable tagging functionality (default: false) */
taggable?: boolean;
/** Placeholder text shown when highlighting tag creation (default: 'Press enter to create a tag') */
tagPlaceholder?: string;
/** Position of tag creation option ('top' | 'bottom', default: 'top') */
tagPosition?: 'top' | 'bottom';
}Usage Example:
<template>
<VueMultiselect
v-model="selectedTags"
:options="availableTags"
:multiple="true"
:taggable="true"
:searchable="true"
@tag="addTag"
tag-placeholder="Add this as new tag"
placeholder="Select or create tags">
</VueMultiselect>
</template>
<script>
export default {
data() {
return {
selectedTags: [],
availableTags: ['Vue.js', 'React', 'Angular']
}
},
methods: {
addTag(newTag) {
const tag = {
name: newTag,
code: newTag.substring(0, 2) + Math.floor((Math.random() * 10000000))
};
this.availableTags.push(tag);
this.selectedTags.push(tag);
}
}
}
</script>Events emitted during tag creation operations.
/**
* Tagging-related events
*/
interface TaggingEvents {
/**
* Emitted when user creates a new tag
* @param searchQuery - Text entered by user for new tag
* @param id - Component identifier
*/
'@tag': (searchQuery: string, id: string | number) => void;
}Control how new tags are created and integrated with existing options.
<template>
<VueMultiselect
v-model="selectedSkills"
:options="skills"
:multiple="true"
:taggable="true"
:searchable="true"
@tag="createSkill"
label="name"
track-by="id"
tag-placeholder="Create new skill"
placeholder="Select or add skills">
</VueMultiselect>
</template>
<script>
export default {
data() {
return {
selectedSkills: [],
skills: [
{ id: 1, name: 'JavaScript', category: 'Programming' },
{ id: 2, name: 'Design', category: 'Creative' }
],
nextId: 3
}
},
methods: {
createSkill(name) {
// Validate tag name
if (name.length < 2) {
alert('Skill name must be at least 2 characters');
return;
}
// Check for duplicates
const exists = this.skills.some(skill =>
skill.name.toLowerCase() === name.toLowerCase()
);
if (exists) {
alert('This skill already exists');
return;
}
// Create new skill
const newSkill = {
id: this.nextId++,
name: name,
category: 'Custom'
};
// Add to options and select it
this.skills.push(newSkill);
this.selectedSkills.push(newSkill);
}
}
}
</script>Control where the tag creation option appears in the dropdown.
/**
* Tag positioning configuration
*/
interface TagPositioningProps {
/**
* Position of tag creation option in dropdown
* 'top' - Show above search results (default)
* 'bottom' - Show below search results
*/
tagPosition?: 'top' | 'bottom';
}Usage Example:
<template>
<VueMultiselect
v-model="selectedCategories"
:options="categories"
:multiple="true"
:taggable="true"
tag-position="bottom"
tag-placeholder="Create new category"
@tag="addCategory">
</VueMultiselect>
</template>
<script>
export default {
methods: {
addCategory(categoryName) {
// Add category to bottom of list
this.categories.push(categoryName);
this.selectedCategories.push(categoryName);
}
}
}
</script>Implement complex tag creation with validation, transformation, and async operations.
<template>
<VueMultiselect
v-model="selectedTags"
:options="availableTags"
:multiple="true"
:taggable="true"
:loading="isCreatingTag"
@tag="createTagAsync"
label="name"
track-by="id"
tag-placeholder="Create and save new tag"
placeholder="Select existing or create new tags">
<template #tag="{ option, remove }">
<span class="custom-tag">
<span>{{ option.name }}</span>
<span v-if="option.isNew" class="tag-new-indicator">NEW</span>
<button @click="remove(option)" class="tag-remove">×</button>
</span>
</template>
</VueMultiselect>
</template>
<script>
export default {
data() {
return {
selectedTags: [],
availableTags: [
{ id: 1, name: 'JavaScript', isNew: false },
{ id: 2, name: 'Vue.js', isNew: false }
],
isCreatingTag: false
}
},
methods: {
async createTagAsync(tagName) {
// Validate tag name
if (!this.validateTagName(tagName)) {
return;
}
this.isCreatingTag = true;
try {
// Transform tag name
const normalizedName = this.normalizeTagName(tagName);
// Check if tag exists (case-insensitive)
const existingTag = this.availableTags.find(tag =>
tag.name.toLowerCase() === normalizedName.toLowerCase()
);
if (existingTag) {
// Select existing tag instead
if (!this.selectedTags.includes(existingTag)) {
this.selectedTags.push(existingTag);
}
return;
}
// Create new tag via API
const newTag = await this.saveTagToServer(normalizedName);
// Add to local state
this.availableTags.push(newTag);
this.selectedTags.push(newTag);
} catch (error) {
console.error('Failed to create tag:', error);
alert('Failed to create tag. Please try again.');
} finally {
this.isCreatingTag = false;
}
},
validateTagName(name) {
if (!name || name.trim().length < 2) {
alert('Tag name must be at least 2 characters');
return false;
}
if (name.length > 50) {
alert('Tag name must be less than 50 characters');
return false;
}
if (!/^[a-zA-Z0-9\s\-.]+$/.test(name)) {
alert('Tag name contains invalid characters');
return false;
}
return true;
},
normalizeTagName(name) {
return name.trim()
.replace(/\s+/g, ' ') // Replace multiple spaces with single space
.toLowerCase()
.replace(/^\w/, c => c.toUpperCase()); // Capitalize first letter
},
async saveTagToServer(name) {
const response = await fetch('/api/tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name })
});
if (!response.ok) {
throw new Error('Failed to save tag');
}
const tag = await response.json();
return {
...tag,
isNew: true
};
}
}
}
</script>
<style>
.custom-tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background: #e3f2fd;
border-radius: 4px;
}
.tag-new-indicator {
font-size: 10px;
background: #4caf50;
color: white;
padding: 1px 4px;
border-radius: 2px;
}
.tag-remove {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
color: #666;
}
</style>Methods for managing tags programmatically.
/**
* Tag management methods
*/
interface TagManagementMethods {
/** Check if a tag with given text already exists */
isExistingOption(query: string): boolean;
/** Add a new tag programmatically */
addTag(tagData: any): void;
/** Remove a tag from available options */
removeTag(tag: any): void;
/** Get all currently selected tags */
getSelectedTags(): any[];
}Use tagging with object-based options for more complex tag structures.
<template>
<VueMultiselect
v-model="selectedTopics"
:options="topics"
:multiple="true"
:taggable="true"
@tag="createTopic"
label="title"
track-by="slug"
tag-placeholder="Create new topic">
</VueMultiselect>
</template>
<script>
export default {
data() {
return {
selectedTopics: [],
topics: [
{ slug: 'javascript', title: 'JavaScript', color: '#f7df1e' },
{ slug: 'vue', title: 'Vue.js', color: '#4fc08d' }
]
}
},
methods: {
createTopic(title) {
const slug = title.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/(^-|-$)/g, '');
const newTopic = {
slug: slug,
title: title,
color: this.getRandomColor(),
isCustom: true
};
this.topics.push(newTopic);
this.selectedTopics.push(newTopic);
},
getRandomColor() {
const colors = ['#ff6b6b', '#4ecdc4', '#45b7d1', '#96ceb4', '#feca57'];
return colors[Math.floor(Math.random() * colors.length)];
}
}
}
</script>Tagging supports standard keyboard shortcuts for tag creation.
/**
* Keyboard shortcuts for tagging
*/
interface TaggingKeyboardShortcuts {
/** Enter key - Create tag from current search query */
'Enter': void;
/** Tab key - Create tag and continue to next field */
'Tab': void;
/** Escape key - Cancel tag creation and close dropdown */
'Escape': void;
}Usage Notes:
Enter while typing to create a new tagTab to create a tag and move focus to next form elementEscape to cancel tag creation and close dropdowntaggable prop is trueInstall with Tessl CLI
npx tessl i tessl/npm-vue-multiselect