Rich Select Box (TRichSelect)

VueJs reactive rich select component inspired in jquery select2 with configurable classes, variants, and most common events. Friendly with utility-first frameworks like TailwindCSS.

Playground:


Props

PropertyTypeDefault valueDescription
idStringundefinedid attribute of the button used as the options toggler
disabledBooleanundefineddisabled attribute of the button used as the options toggler
nameStringundefinedname attribute of the button used as the options toggler
readonlyBooleanundefinedreadonly attribute of the button used as the options toggler
autofocusBooleanundefinedautofocus attribute of the button used as the options toggler
requiredBooleanundefinedrequired attribute of the button used as the options toggler
tabindex[String, Number]undefinedtabindex attribute of the button used as the options toggler
options[Array, Object][]The initial list of options in any of the valid formats accepted
valueAttributeStringundefinedAttribute from the options that should be used as the value of the selected option, accepts dot dotation
textAttributeStringundefinedAttribute from the options that should be used as the text of the selected option, accepts dot dotation
delayNumber250Time in milliseconds between after performs a search when fetching results from custom function
fetchOptionsFunctionundefinedMethod for fetching the options, receives the query as string and the nextPage if apply
minimumInputLengthNumberundefinedMinimum length of the search query to start filtering the results
minimumInputLengthText[Function, String]* (see below)The text that is shown before the user reaches the min length for trigger a query (if set)
minimumResultsForSearchNumberundefinedIf set, The minimum length of the options list needed to show the search box
value (v-model)[Array, String, Number, Object, Boolean]nullThe value for the element
hideSearchBoxBooleanfalseIf set will not show a search box
openOnFocusBooleantrueIf set will open the dropdown when the component is focused
closeOnSelectBooleantrueIf set will close the dropdown once an option is selected
selectOnCloseBooleanfalseIf set will select the highligted option when the dropdown is closed
multiple 2.1.0+BooleanfalseIf set will create a multiselect with tags component
clearableBooleanfalseIf set will show a close button to clear the value of the input
placeholderStringundefinedText that is being shown while no option selected
searchBoxPlaceholderString'Search...'Text that is being shown in the search box while empty
noResultsTextString'No results found'Text that is being shown when the search query doesn't return any result
searchingTextString'Searching...'Text that is being shown while the input is querying the results
loadingMoreResultsTextString'Loading more results...'Text that is being shown when the search box is loading more paginated results
maxHeight[String, Number]300Max height of the dropdown parsed as px, also accepts the height as string in any valid units
classesObject{...} (see below)The default CSS classes
fixedClassesObject{...} (see below)The default CSS Fixed classes shared for all variants
variantsObjectundefinedThe different variants of classes the component have
variant[String, Object]undefinedThe variant that should be used

* The minimumInputLengthText accepts an string or a Function that receive the min input length as first paramater and the current query as the second one, and it should return an string. Example:

(minimumInputLength: number, _query?: string) => {
  return 'Please enter' + minimumInputLength +'  or more characters';
}

Classes and variants format

This component expects an object with classes named after every child element.

The properties in that object are the following:

PropertyDescription
wrapperDiv that wraps the whole component
buttonWrapperDiv that wraps the button that is used as the input
selectButtonButton that is used as the input
selectButtonLabelDiv inside the button that holds the text of the selected option
selectButtonTagWrapperDiv that wraps the tags on the multiselect
selectButtonTagMultiselect option tag
selectButtonTagTextMultiselect option tag text
selectButtonTagDeleteButtonMultiselect option tag delete button
selectButtonTagDeleteButtonIconMultiselect option tag delete button icon
selectButtonPlaceholderDiv that contains the placeholder when no value selected
selectButtonIconIcon with the chevron inside the button
selectButtonClearButtonButton for clear the selected value
selectButtonClearIconCross icon inside the clear button
dropdownDropdown wrapper
dropdownFeedbackSearching, min input length, and no results text
loadingMoreResultsLoading more results text in paginated search
optionsListThe list of options
searchWrapperDiv that wraps the search box
searchBoxThe search box input
optgroupThe div that wraps an optgroup
optionThe default option item
disabledOptionThe option item when its disabled
highlightedOptionThe option item when its highlighted
selectedOptionThe option item when it's selected
selectedHighlightedOptionThe option item when its selected & highlighted
optionContentDiv that wraps the option text
optionLabelThe option text
selectedIconThe option checkmark icon
enterClassVue Custom Transition Class for the options dropdown
enterActiveClassVue Custom Transition Class for the options dropdown
enterToClassVue Custom Transition Class for the options dropdown
leaveClassVue Custom Transition Class for the options dropdown
leaveActiveClassVue Custom Transition Class for the options dropdown
leaveToClassVue Custom Transition Class for the options dropdown

Default fixed classes

As you may know, the fixed classes are merged with the different variants and default classes.

The default fixedClasses on this component are the ones you usually will need as a minimum to ensure this component works as expected.

{
  wrapper: 'relative',
  buttonWrapper: 'inline-block relative w-full',
  selectButton: 'w-full flex text-left justify-between items-center',
  selectButtonLabel: 'block truncate',
  selectButtonTagWrapper: 'flex flex-wrap overflow-hidden',
  selectButtonTag: 'bg-blue-500 block disabled:cursor-not-allowed disabled:opacity-50 duration-100 ease-in-out focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded shadow-sm text-sm text-white transition white-space-no m-0.5 max-w-full overflow-hidden h-8 flex items-center',
  selectButtonTagText: 'px-3',
  selectButtonTagDeleteButton: '-ml-1.5 h-full hover:bg-blue-600 hover:shadow-sm inline-flex items-center px-2 transition',
  selectButtonTagDeleteButtonIcon: 'w-3 h-3',
  selectButtonPlaceholder: 'block truncate',
  selectButtonIcon: 'fill-current flex-shrink-0 ml-1 h-4 w-4',
  selectButtonClearButton: 'flex flex-shrink-0 items-center justify-center absolute right-0 top-0 m-2 h-6 w-6',
  selectButtonClearIcon: 'fill-current h-3 w-3',
  dropdown: 'absolute w-full z-10',
  dropdownFeedback: '',
  loadingMoreResults: '',
  optionsList: 'overflow-auto',
  searchWrapper: 'inline-block w-full',
  searchBox: 'inline-block w-full',
  optgroup: '',
  option: 'cursor-pointer',
  disabledOption: 'opacity-50 cursor-not-allowed',
  highlightedOption: 'cursor-pointer',
  selectedOption: 'cursor-pointer',
  selectedHighlightedOption: 'cursor-pointer',
  optionContent: '',
  optionLabel: 'truncate block',
  selectedIcon: 'fill-current h-4 w-4',
  enterClass: '',
  enterActiveClass: '',
  enterToClass: '',
  leaveClass: '',
  leaveActiveClass: '',
  leaveToClass: '',
};

Default classes

{
  wrapper: '',
  buttonWrapper: '',
  selectButton: 'px-3 py-2 text-black transition duration-100 ease-in-out bg-white border border-gray-300 rounded shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed',
  selectButtonLabel: '',
  selectButtonTagWrapper: '-mx-2 -my-2.5 py-1 pr-8',
  selectButtonTag: 'bg-blue-500 block disabled:cursor-not-allowed disabled:opacity-50 duration-100 ease-in-out focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded shadow-sm text-sm text-white transition white-space-no m-0.5 max-w-full overflow-hidden h-8 flex items-center',
  selectButtonTagText: 'px-3',
  selectButtonTagDeleteButton: '-ml-1.5 h-full hover:bg-blue-600 hover:shadow-sm inline-flex items-center px-2 transition',
  selectButtonTagDeleteButtonIcon: '',
  selectButtonPlaceholder: 'text-gray-400',
  selectButtonIcon: 'text-gray-600',
  selectButtonClearButton: 'hover:bg-blue-100 text-gray-600 rounded transition duration-100 ease-in-out',
  selectButtonClearIcon: '',
  dropdown: '-mt-1 bg-white border-b border-gray-300 border-l border-r rounded-b shadow-sm',
  dropdownFeedback: 'pb-2 px-3 text-gray-400 text-sm',
  loadingMoreResults: 'pb-2 px-3 text-gray-400 text-sm',
  optionsList: '',
  searchWrapper: 'p-2 placeholder-gray-400',
  searchBox: 'px-3 py-2 bg-gray-50 text-sm rounded border focus:outline-none focus:shadow-outline border-gray-300',
  optgroup: 'text-gray-400 uppercase text-xs py-1 px-2 font-semibold',
  option: '',
  disabledOption: '',
  highlightedOption: 'bg-blue-100',
  selectedOption: 'font-semibold bg-gray-100 bg-blue-500 font-semibold text-white',
  selectedHighlightedOption: 'font-semibold bg-gray-100 bg-blue-600 font-semibold text-white',
  optionContent: 'flex justify-between items-center px-3 py-2',
  optionLabel: '',
  selectedIcon: '',
  enterClass: '',
  enterActiveClass: 'opacity-0 transition ease-out duration-100',
  enterToClass: 'opacity-100',
  leaveClass: 'transition ease-in opacity-100',
  leaveActiveClass: '',
  leaveToClass: 'opacity-0 duration-75',
}

Theme Example

{
  fixedClasses: {
    wrapper: 'relative',
    buttonWrapper: 'inline-block relative w-full',
    selectButton: 'w-full flex text-left justify-between items-center',
    selectButtonLabel: 'block truncate',
    selectButtonTagWrapper: 'flex flex-wrap overflow-hidden',
    selectButtonTag: 'bg-blue-500 block disabled:cursor-not-allowed disabled:opacity-50 duration-100 ease-in-out focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded shadow-sm text-sm text-white transition white-space-no m-0.5 max-w-full overflow-hidden h-8 flex items-center',
    selectButtonTagText: 'px-3',
    selectButtonTagDeleteButton: '-ml-1.5 h-full hover:bg-blue-600 hover:shadow-sm inline-flex items-center px-2 transition',
    selectButtonTagDeleteButtonIcon: 'w-3 h-3',
    selectButtonPlaceholder: 'block truncate',
    selectButtonIcon: 'fill-current flex-shrink-0 ml-1 h-4 w-4',
    selectButtonClearButton: 'flex flex-shrink-0 items-center justify-center absolute right-0 top-0 m-2 h-6 w-6',
    selectButtonClearIcon: 'fill-current h-3 w-3',
    dropdown: 'absolute w-full z-10',
    dropdownFeedback: '',
    loadingMoreResults: '',
    optionsList: 'overflow-auto',
    searchWrapper: 'inline-block w-full',
    searchBox: 'inline-block w-full',
    optgroup: '',
    option: 'cursor-pointer',
    disabledOption: 'opacity-50 cursor-not-allowed',
    highlightedOption: 'cursor-pointer',
    selectedOption: 'cursor-pointer',
    selectedHighlightedOption: 'cursor-pointer',
    optionContent: '',
    optionLabel: 'truncate block',
    selectedIcon: 'fill-current h-4 w-4',
    enterClass: '',
    enterActiveClass: '',
    enterToClass: '',
    leaveClass: '',
    leaveActiveClass: '',
    leaveToClass: '',
  },
  classes: {
    wrapper: '',
    buttonWrapper: '',
    selectButton: 'px-3 py-2 text-black transition duration-100 ease-in-out bg-white border border-gray-300 rounded shadow-sm focus:border-blue-500 focus:ring-2 focus:ring-blue-500 focus:outline-none focus:ring-opacity-50 disabled:opacity-50 disabled:cursor-not-allowed',
    selectButtonLabel: '',
    selectButtonTagWrapper: '-mx-2 -my-2.5 py-1 pr-8',
    selectButtonTag: 'bg-blue-500 block disabled:cursor-not-allowed disabled:opacity-50 duration-100 ease-in-out focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50 rounded shadow-sm text-sm text-white transition white-space-no m-0.5 max-w-full overflow-hidden h-8 flex items-center',
    selectButtonTagText: 'px-3',
    selectButtonTagDeleteButton: '-ml-1.5 h-full hover:bg-blue-600 hover:shadow-sm inline-flex items-center px-2 transition',
    selectButtonTagDeleteButtonIcon: '',
    selectButtonPlaceholder: 'text-gray-400',
    selectButtonIcon: 'text-gray-600',
    selectButtonClearButton: 'hover:bg-blue-100 text-gray-600 rounded transition duration-100 ease-in-out',
    selectButtonClearIcon: '',
    dropdown: '-mt-1 bg-white border-b border-gray-300 border-l border-r rounded-b shadow-sm',
    dropdownFeedback: 'pb-2 px-3 text-gray-400 text-sm',
    loadingMoreResults: 'pb-2 px-3 text-gray-400 text-sm',
    optionsList: '',
    searchWrapper: 'p-2 placeholder-gray-400',
    searchBox: 'px-3 py-2 bg-gray-50 text-sm rounded border focus:outline-none focus:shadow-outline border-gray-300',
    optgroup: 'text-gray-400 uppercase text-xs py-1 px-2 font-semibold',
    option: '',
    disabledOption: '',
    highlightedOption: 'bg-blue-100',
    selectedOption: 'font-semibold bg-gray-100 bg-blue-500 font-semibold text-white',
    selectedHighlightedOption: 'font-semibold bg-gray-100 bg-blue-600 font-semibold text-white',
    optionContent: 'flex justify-between items-center px-3 py-2',
    optionLabel: '',
    selectedIcon: '',
    enterClass: '',
    enterActiveClass: 'opacity-0 transition ease-out duration-100',
    enterToClass: 'opacity-100',
    leaveClass: 'transition ease-in opacity-100',
    leaveActiveClass: '',
    leaveToClass: 'opacity-0 duration-75',
  },
  variants: {
    danger: {
      // As explained in the "Theming" sections we only add the classes we want to override
      selectButton: 'border-red-300 bg-red-50 text-red-900',
      selectButtonPlaceholder: 'text-red-200',
      selectButtonIcon: 'text-red-500',
      selectButtonClearButton: 'hover:bg-red-200 text-red-500',
      dropdown: 'bg-red-50 border-red-300'
    },
    //... More variants
  }
}

Options format

This component accepts the options in the same format as the TSelect component. See TSelect options format for more info.

Events

EventArgumentsDescription
inputString (The current value of the select)Emitted every time the value of the v-model change
changeString (The current value of the select)Emitted when the select is blurred and the value was changed since it was focused
focusFocusEventEmitted when the select is focused
blurFocusEventEmitted when the select is blurred
fetch-errorErrorEmitted when the fetchOptions function returns an error
clickMouseEventWhen the button uses as input is clicked

Scoped slots

SlotDescription
labelSelected option label inside the button
optionOption content inside the list of options
searchingTextTo place instead of the default searching text feedback
noResultsTo place instead of the no results text feedback
loadingMoreResultsTextTo place instead of the loading more results text
dropdownUpTo place content above the list of options
dropdownDownTo place content below the list of options

Label slot

Allows you to change the template of the label, it contains the following data:

SlottypeDescription
queryStringThe current search query
optionObjectAnd object with the text and value attribute together with a raw attribute that contains the original option value before normalized
classNameStringThe selectedButtonLabel class in case you can to re-apply it

Example:

Consider that the options come from an ajax query that contains a repo object with info of a GitHub repository

<template>
  <t-rich-select
    :fetch-options="fetchOptions"
    placeholder="select an option"
    value-attribute="full_name"
    text-attribute="full_name"
    :minimum-input-length="1"
  >
    <template
      slot="label"
      slot-scope="{ className, option, query }"
    >
      <div class="flex">
        <span class="flex-shrink-0">
          <img
            class="w-10 h-10 rounded-full"
            :src="option.raw.owner.avatar_url"
          >
        </span>
        <div class="flex flex-col ml-2 text-gray-800">
          <strong>{{ option.raw.full_name }}</strong>
          <span class="text-sm leading-tight text-gray-700">{{ option.raw.description }}</span>
        </div>
      </div>
    </template>
  </t-rich-select>
</template>

<script>
export default {
  methods: {
    fetchOptions (q) {
      return fetch(`https://api.github.com/search/repositories?q=${q}&type=public`)
        .then((response) => response.json())
        .then((data) => ({ results: data.items }))
    }
  }
}
</script>

The example above will look like this: (search and select a repository):

Arrow slot

Since 2.1.2+

Allows you to change the arrow icon on the input

SlottypeDescription
queryStringThe current search query
optionObjectAnd object with the text and value attribute together with a raw attribute that contains the original option value before normalized
classNameStringThe selectedButtonLabel class in case you can to re-apply it

Option slot

Allows you to replace the option content inside the list of options

SlottypeDescription
indexNumberThe index of the option
isHighlightedBooleanIf the option is highlighted
isSelectedBooleanIf the option is selected
optionObjectAnd object with the text and value attributes within raw attribute that contains the original option value before normalized
queryStringThe current search query
classNameStringThe optionContent class in case you can to re-apply it

Example:

Consider that the options come from an ajax query that contains a repo object with info of a GitHub repository

<template>
  <t-rich-select
    :fetch-options="fetchOptions"
    placeholder="Search for a repository"
    value-attribute="full_name"
    text-attribute="full_name"
    :minimum-input-length="1"
    class="max-w-sm mx-auto"
  >
    <template
      slot="option"
      slot-scope="{ index, isHighlighted, isSelected, className, option, query }"
    >
      <div :class="className">
        <span class="flex-shrink-0">
          <img
            class="w-10 h-10 rounded-full"
            :src="option.raw.owner.avatar_url"
          >
        </span>
        <div class="flex flex-col w-full ml-2 text-gray-800">
          <strong>
            {{ option.raw.full_name }}
            <span v-if="isSelected">(Selected)</span>
          </strong>
          <span class="text-sm leading-tight text-gray-700">{{ option.raw.description }}</span>
        </div>
      </div>
    </template>
  </t-rich-select>
</template>

<script>
export default {
  name: 'RichSelectOptionSlot',
  methods: {
    fetchOptions (q) {
      return fetch(`https://api.github.com/search/repositories?q=${q}&type=public`)
        .then(response => response.json())
        .then(data => ({ results: data.items }))
    }
  }
}
</script>

The example above will look like this: (search for a repository):

searchingText slot

Allows you to replace the "searching" text feedback

SlottypeDescription
queryStringThe current search query
textStringThe original searching text
classNameStringThe dropdownFeedback class in case you can to re-apply it

Example:

<t-rich-select>
  <template
    slot="searchingText"
    slot-scope="{ query, text, className }"
  >
    <div :class="className">Please hold on we are looking for options with the query "{{ query }}".</div>
  </template>
</t-rich-select>

noResults slot

Allows you to replace the "no results" text feedback

SlottypeDescription
queryStringThe current search query
textStringThe original searching text
classNameStringThe dropdownFeedback class in case you can to re-apply it

Example:

<t-rich-select>
  <template
    slot="noResults"
    slot-scope="{ query, text, className }"
  >
    <div :class="className">Sorry your "{{ query }}" didnt return any value</div>
  </template>
</t-rich-select>

loadingMoreResultsText slot

Allows you to replace the "loading more results" text feedback below the options

SlottypeDescription
queryStringThe current search query
textStringThe original searching text
classNameStringThe dropdownFeedback class in case you can to re-apply it
nextPageNumber,undefinedThe next page to be loaded

Example:

<t-rich-select>
  <template
    slot="noResults"
    slot-scope="{ query, text, className }"
  >
    <div :class="className">Sorry your "{{ query }}" didnt return any value</div>
  </template>
</t-rich-select>

Allows you to place content above the list of options

SlottypeDescription
queryStringThe current search query
selectedOptionObjectAnd object with the text and value attribute together with a raw attribute that contains the original option value before normalized
optionsArrayThe full list of options (filtered)

Example:

Consider that the options come from an ajax query that contains a repo object with info of a GitHub repository

<t-rich-select>
  <template
    slot="dropdownUp"
    slot-scope="{ query, selectedOption, options }"
  >
    <div>You have {{ options.length }} options</div>
  </template>
</t-rich-select>

Allows you to place content below the list of options

SlottypeDescription
queryStringThe current search query
selectedOptionObjectAnd object with the text and value attribute together with a raw attribute that contains the original option value before normalized
optionsArrayThe full list of options (filtered)

Example:

Useful for place an action button, consider this example for dinamically create an option:

<template>
  <t-rich-select
    v-model="selected"
    :options="options"
  >
    <template
      slot="dropdownDown"
      slot-scope="{ query, selectedOption, options }"
    >
      <div
        v-if="query"
        class="text-center"
      >
        <button
          type="button"
          class="block w-full p-3 text-white bg-blue-500 border hover:bg-blue-600"
          @click="createOption(query)"
        >
          Create {{ query }}
        </button>
      </div>
    </template>
  </t-rich-select>
</template>

<script>
export default {
  data () {
    return {
      selected: null,
      options: ['Option 1', 'Option 2']
    }
  },
  methods: {
    createOption (text) {
      this.options.push(text)
      this.selected = text
    }
  }
}
</script>


The example above will look like this: (search for an option)

Multi-select

Since 2.1.0+

When the multiple prop is set you can use array values on the v-model.

<template>
  <t-rich-select
    multiple
    :close-on-select="false"
    :options="['Option A', 'Option B', 'Option C', 'Option D']"
    placeholder="Select multiple options"
    :value="['Option B', 'Option C']"
  ></t-rich-select>
</template>
For better UX use the multiple options and the :close-on-select="false" options together.

Sign up for our newsletter

Stay up-to-date on news and updates about this project by email.

I will never spam or share your email under any circustance.