Skip to content

Vue 3 multiselect component with single select, multiselect and tagging options (+Tailwind CSS support).

License

Notifications You must be signed in to change notification settings

KamilBeda/multiselect

 
 

Repository files navigation

npm CircleCI npm bundle size (scoped version) Discord npm

Vue 3 Multiselect






Sponsors



Vueform

Vueform is a comprehensive form builder for Vue.js that makes form development a breeze. It standardizes and handles the entire form building process, including:

  • a complete theming and templating system with Tailwind support (similar to @vueform libraries)
  • 25+ form elements with multi-file uploads, date pickers and rich text editor
  • element nesting and repeating
  • 50+ validators with async, dependent and custom rules
  • conditional logic on element & form level
  • breaking forms into steps with form wizard
  • dynamic form rendering with JSON support
  • translating form content and global i18n support.

Learn more: https://vueform.com

Other Vueform libraries:

  • @vueform/slider - Vue 3 slider component with multihandles, tooltips merging and formatting.
  • @vueform/toggle - Vue 3 toggle component with labels, custom slots and styling options.

Comparison with other libraries

Feature @vueform/multiselect vue-multiselect vue-select
Basic Features*
Vue.js 2 support âś“ âś“ âś“
Vue.js 3 support âś“ ~ ~
Single select âś“ âś“ âś“
Multiselect (without tags) âś“ âś“ -
Tags âś“ âś“ âś“
Search & filtering âś“ âś“ âś“
Option groups âś“ âś“ -
Advanced features*
Async search âś“ ~ ~
New option when using tags âś“ ~ âś“
New option when not using tags âś“ - âś“
New option validation âś“ ~ -
Infinite scroll âś“ ~ ~
Object value support âś“ âś“ -
Accents/diacritics sensitivity âś“ ~ ~
Search regex âś“ - ~
Native select support (required) âś“ - ~
Options definition*
Array âś“ âś“ âś“
Object âś“ - -
Array of objects âś“ âś“ âś“
Function (async) âś“ - -
Styling*
Override class names âś“ - -
CSS vars support âś“ - âś“
Class based CSS support âś“ âś“ âś“
Tailwind CSS support
âś“ - -
Support*
Accessibility (a11y) âś“ ~ ~
Internationalization (i18n) âś“ ~ ~
RTL support âś“ âś“ âś“
Typescript support âś“ âś“ âś“
SSR support âś“ âś“ âś“
ES Module support (ESM) âś“ - -
CSP compilant âś“ - -
API*
Events 12 7 11
Slots 14 12 11
Documented API methods 10 0 0
Stats**
Minzipped size 9.7 KB 14.2 KB 20.6 KB
Open issues 18 233 191
Monthly downloads 180k 1.1M 947k
Dependencies 0 0 0
Coverage 100% unknown 96%
Latest realease 2022. 12. 21. 2019. 04. 27. 2022. 12. 18.

~ - partial support / requires manual extension or official support is in progress
* - as of June 13, 2022 - reviewed periodically
** - as of Dec 21, 2022 - reviewed periodically

Disclaimer: based on docs, Github issues and code discovery.

Docs

Demo

Check out our demo.

Installation

npm install @vueform/multiselect

Using with Vue 3

<template>
  <div>
    <Multiselect
      v-model="value"
      :options="options"
    />
  </div>
</template>

<script>
  import Multiselect from '@vueform/multiselect'

  export default {
    components: {
      Multiselect,
    },
    data() {
      return {
        value: null,
        options: [
          'Batman',
          'Robin',
          'Joker',
        ]
      }
    }
  }
</script>

<style src="@vueform/multiselect/themes/default.css"></style>

Using with Vue 2

<template>
  <div>
    <Multiselect
      v-model="value"
      :options="options"
    />
  </div>
</template>

<script>
  import Multiselect from '@vueform/multiselect/dist/multiselect.vue2.js'

  export default {
    components: {
      Multiselect,
    },
    data() {
      return {
        value: null,
        options: [
          'Batman',
          'Robin',
          'Joker',
        ]
      }
    }
  }
</script>

<style src="@vueform/multiselect/themes/default.css"></style>

Using with < Vue 2.7

Switch to <= 2.4.2 to use the Multiselect with Vue.js < 2.7.

Support

Join our Discord channel or open an issue.

Configuration

Basic props

Name {Type} Default Description
mode {string} 'single' Possible values: 'single'|'multiple'|'tags'.
options {array|object|function} [] List of options. Can be:
- an array (eg. [1,2,3])
- an object (eg. {a:1,b:2,c:3})
- an array of objects:
[
  {
    [valueProp]: 1,
    [label]: 'v1',
    disabled:true|false
  },
  //...
]
- a function returning a Promise (async function) with query and select$ param. The select$ represents the Multiselect component and its API can be accessed. The promise should return options as an object or as an array of objects.
When an array of objects is provided it must have properties that equal to :valueProp's, :trackBy's and :label's value.
groups {boolean} false Whether options should be grouped. Example:
{
  groups: true,
  options: [
    {
      [groupLabel]: 'Group label',
      [groupOptions]: {options},
      disabled: true|false,
    }
    //...
  ]
}
The {options} should equal to regular options definition.
groupLabel {string} 'label' The name of the property that contains the label of a group when options are provided in group format and groups is true.
groupOptions {string} 'options' The name of the property that contains the options of a group when options are provided in group format and groups is true.
groupSelect {boolean} true Whether groups can be selected when using multiple or tags mode.
groupHideEmpty {boolean} false Whether groups that have no options by default should be hidden.
required {boolean} false Whether the HTML5 required attribute should be used for multiselect (using an invisible fake input).
infinite {boolean} false Whether the actual option nodes should only be loaded on scroll. The limit option defines how many options are loaded initially and in each new batch.
searchable {boolean} false Whether the options should be searchable.
valueProp {string} 'value' If you provide an array of objects as options this property should be used as the value of the option.
trackBy {string} undefined The name of the property that should be searched when searchable is true and an array of objects are provided as options. If left undefined the label prop will be used instead.
label {string} 'label' If you provide an array of objects as options the value of this property will be displayed as selected option.
disabledProp {string} 'disabled' If you provide an array of objects as options this property should be used to determine whether the option is disabled.
placeholder {string} null The text that should be displayed before any option is selected.
multipleLabel function(value, select$) A function that returns the label to be displayed for selected options when using multiple mode. It receives value as first argument and the multiselect component select$ as second. By default it renders 1 option selected and [n] options selected based on value length.
disabled {boolean} false Whether the input should be disabled for the user (API can still be used programmatically).
inputType {string} 'text' The type attribute of the search input.
autocomplete {string} undefined The autocomplete attribute of the search input.
rtl {boolean} false Whether the multiselect should be right-to-left. It also respects dir="rtl" on any parent element.
max {number} -1 The maximum number of options that can be selected when using multiple or tags mode. If -1 the number of options won't be limited.
limit {number} -1 The maximum number of options that should be displayed. If -1 the number of options won't be limited.
loading {boolean} false Whether a loading spinner should be shown.
id {string} 'multiselect' The id of the multiselect container DOM.
caret {boolean} true Whether should display the caret symbol on the right.
locale {string} null The locale of the multiselect. If a locale is set labels might have an object value with different keys for different locales.
locale {string} 'en' The fallback locale.
noOptionsText `{string object} 'The list is empty'`
noResultsText `{string object} 'No results found'`
openDirection {string} 'bottom' Whether the option list should be displayed above or below the multiselect. Possible values: top|bottom
reverse {boolean} false Whether the option list should be reversed. Only works with groups: false.
regex {regex|string} undefined The regex that search input should be tested against when searchable: true.
strict {boolean} true Whether should regard accents/diacritics in search.
searchStart {boolean} false Whether the search should match the start of the options' trackBys.
searchFilter function(option, select$) null A custom search function that overrides the default search algorithm.
aria object An object containing aria attributes to be added for the multiselect.
classes object An object of class names that gets merged with the default values. Default: {
  container: 'multiselect',
  containerDisabled: 'is-disabled',
  containerOpen: 'is-open',
  containerOpenTop: 'is-open-top',
  containerActive: 'is-active',
  wrapper: 'multiselect-wrapper',
  singleLabel: 'multiselect-single-label',
  singleLabelText: 'multiselect-single-label-text',
  multipleLabel: 'multiselect-multiple-label',
  search: 'multiselect-search',
  tags: 'multiselect-tags',
  tag: 'multiselect-tag',
  tagDisabled: 'is-disabled',
  tagRemove: 'multiselect-tag-remove',
  tagRemoveIcon: 'multiselect-tag-remove-icon',
  tagsSearchWrapper: 'multiselect-tags-search-wrapper',
  tagsSearch: 'multiselect-tags-search',
  tagsSearchCopy: 'multiselect-tags-search-copy',
  placeholder: 'multiselect-placeholder',
  caret: 'multiselect-caret',
  caretOpen: 'is-open',
  clear: 'multiselect-clear',
  clearIcon: 'multiselect-clear-icon',
  spinner: 'multiselect-spinner',
  dropdown: 'multiselect-dropdown',
  dropdownTop: 'is-top',
  dropdownHidden: 'is-hidden',
  options: 'multiselect-options',
  optionsTop: 'is-top',
  group: 'multiselect-group',
  groupLabel: 'multiselect-group-label',
  groupLabelPointable: 'is-pointable',
  groupLabelPointed: 'is-pointed',
  groupLabelSelected: 'is-selected',
  groupLabelDisabled: 'is-disabled',
  groupLabelSelectedPointed: 'is-selected is-pointed',
  groupLabelSelectedDisabled: 'is-selected is-disabled',
  groupOptions: 'multiselect-group-options',
  option: 'multiselect-option',
  optionPointed: 'is-pointed',
  optionSelected: 'is-selected',
  optionDisabled: 'is-disabled',
  optionSelectedPointed: 'is-selected is-pointed',
  optionSelectedDisabled: 'is-selected is-disabled',
  noOptions: 'multiselect-no-options',
  noResults: 'multiselect-no-results',
  fakeInput: 'multiselect-fake-input',
  assist: 'multiselect-assistive-text'
  spacer: 'multiselect-spacer'
}
Vueform

Advanced Props

Name {Type} Default Description
allowAbsent {boolean} false Whether values should be allowed which are not part of options even when using object: false. The selected values which are not part of the option list will have the same value and label. This can be useful if you're using an async option list with an array of string options as a result where both labels and values will be the same and you want to have default values which are not part of the initially resolved options. Example #13
canDeselect {boolean} true Whether a selected option can be deselected when using single mode.
canClear {boolean} true Whether option(s) can be cleared.
clearOnSearch {boolean} false Whether the option list should be cleared when a new character is typed before loading new options list, when using async options.
clearOnSelect {boolean} true Whether the option list should be cleared upon selecting an option when using async options.
closeOnSelect {boolean} true Whether the option list should be closed upon selecting an option.
closeOnDeselect {boolean} true Whether the option list should be closed upon deselecting an option.
clearOnBlur {boolean} true Whether the search should be cleared when the input is blurred when searchable: true.
delay {number} -1 The delay in milliseconds that should occur between the last typed character and refreshing an async option list. If -1 the option list will not refresh when the search query changes. If 0 it will refresh without delay.
filterResults {boolean} true Whether option list should be filtered by search query. This may be set to false if you are handling filtering manually when returning async options.
minChars {number} 0 The minimum number of characters that should be typed to refresh async option list. If 0 it will refresh even when the search field becomes empty.
resolveOnLoad {boolean} true Whether async options should be loaded initially (with an empty query). This should be true if you are planning to load non-object value(s) initially while using async options (to fetch matching objects for values).
appendNewTag {boolean} true Deprecated 2.3.0: use appendNewOption instead.
Whether it should append new tag automatically to option list when using tags mode with createTag. If set to false you need to take care of appending a new tag to the provided :options list upon @tag event.
createTag {boolean} false Deprecated 2.3.0: use createOption instead.
Whether it should allow creating new tags based on search query when using tags mode.
addTagOn {array} ['enter'] Deprecated 2.3.0: use addOptionOn instead.
The list of keys that creates a new tag while typing in the search field when having createTag enabled. Possible values: 'enter'|'space'|'tab'|';'|','.
appendNewOption {boolean} true Whether it should append new option automatically to option list when searchable and createTag are enabled. If set to false you need to take care of appending a new option to the provided :options list upon @option event.
createOption {boolean} false Whether it should allow creating new options based on search query when searchable is enabled.
addOptionOn {array} ['enter'] The list of keys that creates a new option while typing in the search field when having createOption enabled. Possible values: 'enter'|'space'|'tab'|';'|','.
onCreate function(option, select$) Transforms the created tag before being added when createOption is enabled. It receives the original option as first param, which is the object that would be added to the option list ({value: 'Value', label: 'Label'}) and the Multiselect component as the second. It should return an object that contains at least the keys defined by valueProp, label & trackBy options. If defined and returns false the option will not be added (the add can be handled manually by updating options & v-model).
hideSelected {boolean} true Whether selected options should be excluded from the option list when using multiple or tags mode.
showOptions {boolean} true Whether option list should be displayed. Can be used to create free-typed tags.
object {boolean} false Whether the value should be stored as an object.
If false:
value: ['js','jsx','ts']
If true:
value: [
  {value:'js',label:'Javascript'},
  {value:'jsx',label:'JSX'},
  {value:'ts',label:'Typescript'}
]
attrs {object} {} HTML attributes to add to the input field when search is enabled.
nativeSupport {boolean} false Whether hidden input fields should be appended to achieve native data handling.

API methods

Name Params Description
open Opens the options list.
close Closes the options list.
select option Selects an option based on its value.
deselect option Deselects an option based on its value.
remove option Alias for deselect.
selectAll Selects all options if mode is tags or multiple.
clear Deselects all selected options.
clearSearch Clears current search query.
refreshOptions callback Refreshes async options list.
setPointer option Points an option based on its value.

To access API use ref on Multiselect component:

<Multiselect
  v-model="value"
  :options="options"
  ref="multiselect"
/>
// eg:
mounted() {
  this.$refs.multiselect.open()
}

To programmatically open and focus the multiselect, call focus() on the element:

mounted() {
  this.$refs.multiselect.$el.focus()
}

Events

Event Attributes Description
@change value, select$ Emitted after the value is changed.
@close select$ Emitted after closing the option list.
@deselect option, select$ Emitted after an option is deselected or a tag is removed.
@open select$ Emitted after opening the option list.
@search-change query, select$ Emitted after a character is typed.
@select option, select$ Emitted after an option or tag is selected.
@tag query, select$ Deprecated 2.3.0: use @create instead. Emitted after enter is hit when a new tag is being created.
@option query, select$ Deprecated 2.6.0: use @create instead. Emitted after enter is hit when a new option is being created.
@create query, select$ Emitted after enter is hit when a new option is being created.
@clear select$ Emitted when the options are cleared.
@paste Event, select$ Emitted when value is pasted into the search field.
@keydown Event, select$ Emitted on keydown.
@keyup Event, select$ Emitted on keyup.
@max select$ Emitted when max is reached when in multiple or tags mode.

The select$ param in each event is the Multiselect component's instance.

Slots

Slot Attributes Description
placeholder Rendered as placeholder when the multiselect does not have value and placeholder prop is defined.
afterlist Rendered after the options list.
beforelist Rendered before the options list.
multiplelabel values Rendered when using multiple mode and options are selected. By default it renders the return value of multipleLabel function.
nooptions Rendered when the options list is empty. By default renders noOptionsText.
noresults Rendered when there are no search results. By default renders noResultsText.
grouplabel group, isPointed, isSelected Renders an option group label. The isPointed and isSelected props are function that used be used like isPointed(option) to determine the state.
option option, isPointed, isSelected, search Renders an option in options list. The isPointed and isSelected props are function that used be used like isPointed(option) to determine the state.
singlelabel value Rendered when using single mode and an option is selected. By default it renders the :label if the selected option.
tag option, handleTagRemove, disabled Renders a tag when using tags mode. When disabled the remove icon should not be displayed. The handleTagRemove prop should be used to trigger the removal of the tag.
caret Renders a small triangle on the right side of the multiselect.
clear clear Renders a remove icon if the multiselect has any value. The clear method should be used on mousedown event.
spinner Renders a loader icon when async options are being fetched.
infinite Renders a loader icon when infinite scroll is in progress.

Note: we don't use camelCase because they are normalized back to lowercase when written in DOM.

Styling

Styling with CSS vars

The following CSS variables can be used to customize multiselect when using default.css:

--ms-font-size: 1rem;
--ms-line-height: 1.375;
--ms-bg: #FFFFFF;
--ms-bg-disabled: #F3F4F6;
--ms-border-color: #D1D5DB;
--ms-border-width: 1px;
--ms-border-color-active: #D1D5DB;
--ms-border-width-active: 1px;
--ms-radius: 4px;
--ms-py: 0.5rem;
--ms-px: 0.875rem;
--ms-ring-width: 3px;
--ms-ring-color: #10B98130;
--ms-placeholder-color: #9CA3AF;
--ms-max-height: 10rem;

--ms-spinner-color: #10B981;
--ms-caret-color: #999999;
--ms-clear-color: #999999;
--ms-clear-color-hover: #000000;
  
--ms-tag-font-size: 0.875rem;
--ms-tag-line-height: 1.25rem;
--ms-tag-font-weight: 600;
--ms-tag-bg: #10B981;
--ms-tag-bg-disabled: #9CA3AF;
--ms-tag-color: #FFFFFF;
--ms-tag-color-disabled: #FFFFFF;
--ms-tag-radius: 4px;
--ms-tag-py: 0.125rem;
--ms-tag-px: 0.5rem;
--ms-tag-my: 0.25rem;
--ms-tag-mx: 0.25rem;

--ms-tag-remove-radius: 4px;
--ms-tag-remove-py: 0.25rem;
--ms-tag-remove-px: 0.25rem;
--ms-tag-remove-my: 0rem;
--ms-tag-remove-mx: 0.125rem;

--ms-dropdown-bg: #FFFFFF;
--ms-dropdown-border-color: #D1D5DB;
--ms-dropdown-border-width: 1px;
--ms-dropdown-radius: 4px;

--ms-group-label-py: 0.3rem;
--ms-group-label-px: 0.75rem;
--ms-group-label-line-height: 1.375;
--ms-group-label-bg: #E5E7EB;
--ms-group-label-color: #374151;
--ms-group-label-bg-pointed: #D1D5DB;
--ms-group-label-color-pointed: #374151;
--ms-group-label-bg-disabled: #F3F4F6;
--ms-group-label-color-disabled: #D1D5DB;
--ms-group-label-bg-selected: #059669;
--ms-group-label-color-selected: #FFFFFF;
--ms-group-label-bg-selected-pointed: #0c9e70;
--ms-group-label-color-selected-pointed: #FFFFFF;
--ms-group-label-bg-selected-disabled: #75cfb1;
--ms-group-label-color-selected-disabled: #D1FAE5;

--ms-option-font-size: 1rem;
--ms-option-line-height: 1.375;
--ms-option-bg-pointed: #FFFFFF;
--ms-option-color-pointed: #1F2937;
--ms-option-bg-selected: #10B981;
--ms-option-color-selected: #FFFFFF;
--ms-option-bg-disabled: #FFFFFF;
--ms-option-color-disabled: #D1D5DB;
--ms-option-bg-selected-pointed: #26C08E;
--ms-option-color-selected-pointed: #FFFFFF;
--ms-option-bg-selected-disabled: #FFFFFF;
--ms-option-color-selected-disabled: #D1FAE5;
--ms-option-py: 0.5rem;
--ms-option-px: 0.75rem;

--ms-empty-color: #4B5563;

Override them globally:

:root {
  --ms-tag-bg: #059669;
  --ms-tag-color: #D1FAE5;
  --ms-tag-radius: 9999px;
  --ms-tag-font-weight: 400;
}

Or on an instance level:

<Multiselect
  v-model="value"
  :options="options"
  class="multiselect-green"
/>

<Multiselect
  v-model="value"
  :options="options"
  class="multiselect-blue"
/>
.multiselect-green {
  --ms-tag-bg: #D1FAE5;
  --ms-tag-color: #059669;
}

.multiselect-blue {
  --ms-tag-bg: #DBEAFE;
  --ms-tag-color: #2563EB;
}

Styling with Tailwind CSS

To use Multiselect with Tailwind CSS first you need install npm i -D mini-svg-data-url and add background images to tailwind.config.js:

// tailwind.config.js

const svgToDataUri = require('mini-svg-data-uri')

module.exports = {
  theme: {
    extend: {
      backgroundImage: (theme) => ({
        'multiselect-caret': `url("${svgToDataUri(
          `<svg viewBox="0 0 320 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z"></path></svg>`,
        )}")`,
        'multiselect-spinner': `url("${svgToDataUri(
          `<svg viewBox="0 0 512 512" fill="${theme('colors.green.500')}" xmlns="http://www.w3.org/2000/svg"><path d="M456.433 371.72l-27.79-16.045c-7.192-4.152-10.052-13.136-6.487-20.636 25.82-54.328 23.566-118.602-6.768-171.03-30.265-52.529-84.802-86.621-144.76-91.424C262.35 71.922 256 64.953 256 56.649V24.56c0-9.31 7.916-16.609 17.204-15.96 81.795 5.717 156.412 51.902 197.611 123.408 41.301 71.385 43.99 159.096 8.042 232.792-4.082 8.369-14.361 11.575-22.424 6.92z"></path></svg>`,
        )}")`,
        'multiselect-remove': `url("${svgToDataUri(
          `<svg viewBox="0 0 320 512" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M207.6 256l107.72-107.72c6.23-6.23 6.23-16.34 0-22.58l-25.03-25.03c-6.23-6.23-16.34-6.23-22.58 0L160 208.4 52.28 100.68c-6.23-6.23-16.34-6.23-22.58 0L4.68 125.7c-6.23 6.23-6.23 16.34 0 22.58L112.4 256 4.68 363.72c-6.23 6.23-6.23 16.34 0 22.58l25.03 25.03c6.23 6.23 16.34 6.23 22.58 0L160 303.6l107.72 107.72c6.23 6.23 16.34 6.23 22.58 0l25.03-25.03c6.23-6.23 6.23-16.34 0-22.58L207.6 256z"></path></svg>`,
        )}")`,
      })
    },
  }
}

Then you need to import themes/tailwind.css to you main component:

<template>
  <div id="app">
    <Multiselect ... />
  </div>
</template>

<script>
  // ...
</script>

<style>
  @import '@vueform/multiselect/themes/tailwind.css';
  /* or */
  /* @import './path/to/node_modules/@vueform/multiselect/themes/tailwind.css'; */
</style>

Using :classes prop

Alternatively you can define class names directly by passing them to the Multiselect component via classes property. When using this approach you don't need to import tailwind.css. Here's a default styling for Tailwind CSS (the same included in tailwind.css):

<Multiselect ... :classes="{
  container: 'relative mx-auto w-full flex items-center justify-end box-border cursor-pointer border border-gray-300 rounded bg-white text-base leading-snug outline-none',
  containerDisabled: 'cursor-default bg-gray-100',
  containerOpen: 'rounded-b-none',
  containerOpenTop: 'rounded-t-none',
  containerActive: 'ring ring-green-500 ring-opacity-30',
  singleLabel: 'flex items-center h-full max-w-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 pr-16 box-border rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5',
  singleLabelText: 'overflow-ellipsis overflow-hidden block whitespace-nowrap max-w-full',
  multipleLabel: 'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5',
  search: 'w-full absolute inset-0 outline-none focus:ring-0 appearance-none box-border border-0 text-base font-sans bg-white rounded pl-3.5 rtl:pl-0 rtl:pr-3.5',
  tags: 'flex-grow flex-shrink flex flex-wrap items-center mt-1 pl-2 rtl:pl-0 rtl:pr-2',
  tag: 'bg-green-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap rtl:pl-0 rtl:pr-2 rtl:mr-0 rtl:ml-1',
  tagDisabled: 'pr-2 opacity-50 rtl:pl-2',
  tagRemove: 'flex items-center justify-center p-1 mx-0.5 rounded-sm hover:bg-black hover:bg-opacity-10 group',
  tagRemoveIcon: 'bg-multiselect-remove bg-center bg-no-repeat opacity-30 inline-block w-3 h-3 group-hover:opacity-60',
  tagsSearchWrapper: 'inline-block relative mx-1 mb-1 flex-grow flex-shrink h-full',
  tagsSearch: 'absolute inset-0 border-0 outline-none focus:ring-0 appearance-none p-0 text-base font-sans box-border w-full',
  tagsSearchCopy: 'invisible whitespace-pre-wrap inline-block h-px',
  placeholder: 'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 rtl:left-auto rtl:right-0 rtl:pl-0 rtl:pr-3.5',
  caret: 'bg-multiselect-caret bg-center bg-no-repeat w-2.5 h-4 py-px box-content mr-3.5 relative z-10 opacity-40 flex-shrink-0 flex-grow-0 transition-transform transform pointer-events-none rtl:mr-0 rtl:ml-3.5',
  caretOpen: 'rotate-180 pointer-events-auto',
  clear: 'pr-3.5 relative z-10 opacity-40 transition duration-300 flex-shrink-0 flex-grow-0 flex hover:opacity-80 rtl:pr-0 rtl:pl-3.5',
  clearIcon: 'bg-multiselect-remove bg-center bg-no-repeat w-2.5 h-4 py-px box-content inline-block',
  spinner: 'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin flex-shrink-0 flex-grow-0 rtl:mr-0 rtl:ml-3.5',
  inifite: 'flex items-center justify-center w-full',
  inifiteSpinner: 'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 animate-spin flex-shrink-0 flex-grow-0 m-3.5',
  dropdown: 'max-h-60 absolute -left-px -right-px bottom-0 transform translate-y-full border border-gray-300 -mt-px overflow-y-scroll z-50 bg-white flex flex-col rounded-b',
  dropdownTop: '-translate-y-full top-px bottom-auto rounded-b-none rounded-t',
  dropdownHidden: 'hidden',
  options: 'flex flex-col p-0 m-0 list-none',
  optionsTop: '',
  group: 'p-0 m-0',
  groupLabel: 'flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 cursor-default leading-normal',
  groupLabelPointable: 'cursor-pointer',
  groupLabelPointed: 'bg-gray-300 text-gray-700',
  groupLabelSelected: 'bg-green-600 text-white',
  groupLabelDisabled: 'bg-gray-100 text-gray-300 cursor-not-allowed',
  groupLabelSelectedPointed: 'bg-green-600 text-white opacity-90',
  groupLabelSelectedDisabled: 'text-green-100 bg-green-600 bg-opacity-50 cursor-not-allowed',
  groupOptions: 'p-0 m-0',
  option: 'flex items-center justify-start box-border text-left cursor-pointer text-base leading-snug py-2 px-3',
  optionPointed: 'text-gray-800 bg-gray-100',
  optionSelected: 'text-white bg-green-500',
  optionDisabled: 'text-gray-300 cursor-not-allowed',
  optionSelectedPointed: 'text-white bg-green-500 opacity-90',
  optionSelectedDisabled: 'text-green-100 bg-green-500 bg-opacity-50 cursor-not-allowed',
  noOptions: 'py-2 px-3 text-gray-600 bg-white text-left',
  noResults: 'py-2 px-3 text-gray-600 bg-white text-left',
  fakeInput: 'bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent',
  spacer: 'h-9 py-px box-content',
}" />

Certain classes has different states which are merged to the base class when the state is active. For example dropdown will be merged with dropdownTop when open-direction: 'top' resulting in the following classes: absolute -left-px -right-px bottom-0 transform translate-y-full border border-gray-300 -mt-px overflow-y-scroll z-50 bg-white flex flex-col rounded-b -translate-y-full top-px bottom-auto rounded-b-none rounded-t

The same is true for container, tag, options, groupLabel and option classes.

In case you need to override the same type of utility you might use @neojp/tailwind-important-variant for eg. bg-green-500!.

Examples

Single select

<Multiselect
  v-model="value"
  :options="['Batman', 'Robin', 'Joker']"
/>

JSFiddle - Example #1

Multiselect with object options

<Multiselect
  v-model="value"
  mode="multiple"
  :close-on-select="false"
  :options="{
    batman: 'Batman',
    robin: 'Robin',
    joker: 'Joker'
  }"
/>

JSFiddle - Example #2

Multiselect with disabled options

<Multiselect
  v-model="value"
  mode="multiple"
  :close-on-select="false"
  :options="[
    { value: 'batman', label: 'Batman' },
    { value: 'robin', label: 'Robin', disabled: true },
    { value: 'joker', label: 'Joker' },
  ]"
/>

JSFiddle - Example #3

Multiselect with groups

<Multiselect
  v-model="value"
  mode="multiple"
  :close-on-select="false"
  :groups="true"
  :options="[
    {
      label: 'DC',
      options: ['Batman', 'Robin', 'Joker'],
    },
    {
      label: 'Marvel',
      options: ['Spiderman', 'Iron Man', 'Captain America'],
    },
  ]"
/>

JSFiddle - Example #4

Tags with search, create and array of objects options

<Multiselect
  v-model="value"
  mode="tags"
  :close-on-select="false"
  :searchable="true"
  :create-option="true"
  :options="[
    { value: 'batman', label: 'Batman' },
    { value: 'robin', label: 'Robin' },
    { value: 'joker', label: 'Joker' },
  ]"
/>

JSFiddle - Example #5

Autocomplete with async options

<Multiselect
  v-model="value"
  placeholder="Choose a programming language"
  :filter-results="false"
  :min-chars="1"
  :resolve-on-load="false"
  :delay="0"
  :searchable="true"
  :options="async function(query) {
    return await fetchLanguages(query) // check JS block in JSFiddle for implementation
  }"
/>

JSFiddle - Example #6

Tags with async options

<Multiselect
  v-model="value"
  mode="tags"
  placeholder="Choose your stack"
  :close-on-select="false"
  :filter-results="false"
  :min-chars="1"
  :resolve-on-load="false"
  :delay="0"
  :searchable="true"
  :options="async function(query, select$) {
    return await fetchLanguages(query) // check JS block in JSFiddle for implementation
  }"
/>

JSFiddle - Example #7

Select with custom options slot

<Multiselect
  v-model="value"
  placeholder="Select your character"
  label="name"
  :options="[
    { value: 'captainamerica', name: 'Captain America', icon: 'https://cdn2.iconfinder.com/data/icons/avengers-filled/48/03_-_Captain_America_-_infinity_war_-_end_game_-_marvel_-_avengers_-_super_hero-512.png' },
    { value: 'spiderman', name: 'Spiderman', icon: 'https://cdn2.iconfinder.com/data/icons/avengers-filled/48/12_-_Spiderman_-_infinity_war_-_end_game_-_marvel_-_avengers_-_super_hero-512.png' },
    { value: 'ironman', name: 'Iron Man', icon: 'https://cdn2.iconfinder.com/data/icons/avengers-filled/48/02_-_IRONMAN_-_infinity_war_-_end_game_-_marvel_-_avengers_-_super_hero-512.png' },
  ]"
>
  <template v-slot:singlelabel="{ value }">
    <div class="multiselect-single-label">
      <img class="character-label-icon" :src="value.icon"> {{ value.name }}
    </div>
  </template>

  <template v-slot:option="{ option }">
    <img class="character-option-icon" :src="option.icon"> {{ option.name }}
  </template>
</Multiselect>

JSFiddle - Example #8

Multiselect with custom label slot

<Multiselect
  v-model="value"
  mode="multiple"
  placeholder="Select your characters"
  :close-on-select="false"
  :options="{
    batman: 'Batman',
    robin: 'Robin',
    joker: 'Joker'
  }"
>
  <template v-slot:multiplelabel="{ values }">
    <div class="multiselect-multiple-label">
      {{ values.length }} characters selected
    </div>
  </template>
</Multiselect>

JSFiddle - Example #9

Tags with custom tags slot

<template>
  <Multiselect
    v-model="value"
    mode="tags"
    placeholder="Select employees"
    track-by="name"
    label="name"
    :close-on-select="false"
    :searchable="true"
    :options="[
      { value: 'judy', name: 'Judy', image: 'https://randomuser.me/api/portraits/med/women/1.jpg' },
      { value: 'jane', name: 'Jane', image: 'https://randomuser.me/api/portraits/med/women/2.jpg' },
      { value: 'john', name: 'John', image: 'https://randomuser.me/api/portraits/med/men/1.jpg' },
      { value: 'joe', name: 'Joe', image: 'https://randomuser.me/api/portraits/med/men/2.jpg' }
    ]"
  >
      <template v-slot:tag="{ option, handleTagRemove, disabled }">
        <div
          class="multiselect-tag is-user"
          :class="{
            'is-disabled': disabled
          }"
        >
          <img :src="option.image">
          {{ option.name }}
          <span
            v-if="!disabled"
            class="multiselect-tag-remove"
            @click="handleTagRemove(option, $event)"
          >
            <span class="multiselect-tag-remove-icon"></span>
          </span>
        </div>
      </template>
  </Multiselect>
</template>

<style>
  .multiselect-tag.is-user {
    padding: 5px 8px;
    border-radius: 22px;
    background: #35495e;
    margin: 3px 3px 8px;
  }

  .multiselect-tag.is-user img {
    width: 18px;
    border-radius: 50%;
    height: 18px;
    margin-right: 8px;
    border: 2px solid #ffffffbf;
  }

  .multiselect-tag.is-user i:before {
    color: #ffffff;
    border-radius: 50%;;
  }

  .user-image {
    margin: 0 6px 0 0;
    border-radius: 50%;
    height: 22px;
  }
</style>

JSFiddle - Example #10

Async options with default values

When using resolveOnLoad: false we can add default values with object: true and providing options as objects, containing both label and value props. This is because option list is not resolved when the component is mounted so Multiselect has no idea of what option labels should be if only plain values were provided.

<template>
  <Multiselect
    mode="tags"
    v-model="value"
    placeholder="Select options"
    :close-on-select="false"
    :searchable="true"
    :object="true"
    :resolve-on-load="false"
    :delay="0"
    :min-chars="1"
    :options="async (query) => {
      return await fetchLanguages(query)
    }"
  />
</template>
<script>
export default {
  data: () => ({
    value: [
      { value: 'Java', label: 'Java' },
      { value: 'JavaScript', label: 'JavaScript' },
    ]
  })
}
</script>

JSFiddle - Example #11

Default values that are not among the options using object: true

If we want to add default values without having to add them to options list we can use object: true and provide them as objects, containing both label and value props. This is because if a plain value is not among Multiselect options it has no idea of what option label should be.

<template>
  <Multiselect
    mode="tags"
    v-model="value"
    placeholder="Select options"
    :close-on-select="false"
    :searchable="true"
    :object="true"
    :resolve-on-load="false"
    :delay="0"
    :min-chars="1"
    :options="async (query) => {
      return await fetchLanguages(query)
    }"
  />
</template>
<script>
export default {
  data: () => ({
    value: [
      { value: 'Java', label: 'Java' },
      { value: 'JavaScript', label: 'JavaScript' },
    ]
  })
}
</script>

JSFiddle - Example #12

Default values that are not among the options using allowAbsent: true

If our async option list returns an array of strings we can use allowAbsent: true to allow value(s) which are not among the option list. The reason why this only works with an array of strings option list is because plain values like Java and JavaScript will use the same string for label and value.

<template>
  <Multiselect
    mode="tags"
    v-model="value"
    placeholder="Select options"
    :allow-absent="true"
    :close-on-select="false"
    :searchable="true"
    :resolve-on-load="false"
    :delay="0"
    :min-chars="1"
    :options="async (query) => {
      return await fetchLanguages(query)
    }"
  />
</template>
<script>
export default {
  data: () => ({
    value: [
      'Java',
      'JavaScript',
    ]
  })
}
</script>

JSFiddle - Example #13

Manage created tag asynchronously

Search is restricted by regex and tag creation is controlled by onCreate(option, select$).

<template>
  <Multiselect
    mode="tags"
    v-model="value"
    placeholder="Accepts numbers <= 67 (delay: 1000ms)"
    :options="[]"
    :create-option="true"
    :searchable="true"
    :regex="/\d/"
    :on-create="handleTagCreate"
  />
</template>
<script>
export default {
  methods: {
    handleTagCreate: async (option, select$) => {
      // Do not allow create tags above 67
      if (parseInt(option.value) > 67) {
        alert(`${option.value} is not allowed. Option must by <= 67.`)

        // If returns `false` the tag will not be added
        return false
      }

      // Async request (eg. for validating)
      await new Promise((resolve, reject) => {
        setTimeout(() => {
          resolve()
        }, 1000)
      })

      // Modifying option label
      option.label = option.label + ' - confirmed'

      return option
    }
  }
}
</script>

JSFiddle - Example #14

Load async options from API on open with infinite scroll

Options are not loaded initially, only when the users clicks the dropdown the first time. It also virtualizes the option list with infinite: true even large list of options can be loaded.

<Multiselect
  v-model="value"
  mode="tags"
  placeholder="Choose your stack"
  :close-on-select="false"
  :filter-results="false"
  :min-chars="0"
  :resolve-on-load="false"
  :infinite="true"
  :limit="10"
  :clear-on-search="true"
  :delay="0"
  :searchable="true"
  :options="async (query) => {
    return await fetchLanguages(query)
  }"
  @open="(select$) => {
    if (select$.noOptions) {
      select$.resolveOptions()
    }
  }"
/>

JSFiddle - Example #15

Multiselect with localized texts

Options are not loaded initially, only when the users clicks the dropdown the first time. It also virtualizes the option list with infinite: true even large list of options can be loaded.

<template>
  <Multiselect
    v-model="value"
    locale="de"
    fallback-locale="en"
    :options="[
      { value: 1, label: { en: 'One', de: 'Eins' } },
      { value: 2, label: { en: 'Two' } },
      { value: 3, label: { es: 'Tres'} },
      { value: 4, label: 'Four' },
    ]"
  />
</template>
<script>
export default {
  data: () => ({
    value: [1]
  })
}
</script>

JSFiddle - Example #16

License

MIT

About

Vue 3 multiselect component with single select, multiselect and tagging options (+Tailwind CSS support).

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • JavaScript 86.2%
  • SCSS 6.6%
  • Vue 5.4%
  • CSS 1.8%