Skip to content

Combobox Component

The Combobox component provides a powerful searchable select input with autocomplete functionality built with Vue 3 and Headless UI. It supports asynchronous data loading, single/multiple selection, custom rendering, and various customization options.

Installation

To use the Combobox component you need to install the @vueuse/core and @headlessui/vue packages.

bash
yarn add @robuust-digital/vue-components @vueuse/core @headlessui/vue

Import the component from the combobox package

js
import { Combobox } from '@robuust-digital/vue-components/combobox';

Import CSS

css
@import "@robuust-digital/vue-components/combobox/css";

Basic Usage

vue
<template>
  <Combobox
    v-model="single"
    id="basic-example"
    :on-search="mockSearch"
    placeholder="Search fruits..."
  />
</template>

API Integration Example

Real-world example using JSONPlaceholder API to search users:

vue
<template>
  <Combobox
    v-model="apiUsers"
    id="api-example"
    endpoint="users"
    :on-search="comboboxSearch"
    :display-value="customDisplayValue"
    :option-text="customOptionText"
    placeholder="Search users..."
    clearable
  />
</template>

<script setup>
/**
 * API search example
 * 
 * @param {String} searchValue - Search query
 * @param {Object} params - Additional request params
 * @param {String} endpoint - API endpoint
 */
const comboboxSearch = async (searchValue, params, endpoint) => {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/${endpoint}${searchValue ? `?username=${searchValue}` : ''}`
  );

  if (response.ok) {
    const data = await response.json();
    return { data };
  }

  return { data: [], error: 'An error occurred' };
};
</script>

Props

PropTypeDefaultDescription
idStringRequiredUnique identifier for the combobox
modelValueAnynullv-model binding value
endpointStringundefinedAPI endpoint for search requests
requestParamsObject{}Additional params for API requests
manualInputBooleanfalseAllow manual input without selection
responseDataFunctiondata => dataTransform API response data
displayValueFunctionitem => item?.nameFormat selected value display
optionTextFunctionitem => item?.nameFormat option text in dropdown
disabledBooleanfalseDisable the combobox
minLengthNumber2Minimum characters before search
itemKeyString'id'Unique key for options
clearableBooleanfalseShow clear button
multipleBooleanfalseEnable multiple selection
searchOnlyBooleanfalseSearch-only mode without selection
iconObject, FunctionnullCustom icon component
prefixIconObject, FunctionnullCustom left icon component
sizeString'base'Size variant ('sm' or 'base')
debounceNumber150Debounce time for search in milliseconds
minLoadingTimeNumber0Minimum loading indicator display time
onCancelFunctionnullCallback to cancel pending requests

Events

EventPayloadDescription
update:modelValueAnyEmitted when selection changes
update:requestParamsObjectEmitted when request params change
combobox:noResultsStringEmitted when search returns no results
combobox:errorObject, StringEmitted when search encounters an error

Advanced Examples

Multiple Selection with Custom Display

vue
<template>
  <Combobox
    v-model="multiple"
    id="multiple-custom"
    :on-search="comboboxSearch"
    endpoint="users"
    multiple
    clearable
    :display-value="customDisplayValue"
    :option-text="customOptionText"
    placeholder="Select multiple users..."
  >
    <template #chip="{ optionText, option, removeOption }">
      <Badge color="blue" size="sm">
        {{ optionText }}
        <button @click="removeOption(option)">×</button>
      </Badge>
    </template>
  </Combobox>
</template>

Custom Option Rendering

vue
<template>
  <Combobox
    v-model="single"
    id="custom-option"
    :on-search="comboboxSearch"
    endpoint="users"
    clearable
  >
    <template #option="{ option }">
      <div class="flex flex-col">
        <strong>{{ option.username }}</strong>
        <small class="text-slate-500">{{ option.email }}</small>
      </div>
    </template>
  </Combobox>
</template>

Request Cancellation

Example showing how to cancel pending requests when a new search is initiated:

vue
<template>
  <Combobox
    v-model="single"
    id="cancel-example"
    :on-search="searchWithCancel"
    :on-cancel="(controller) => controller.abort()"
    placeholder="Search with cancellation..."
  />
</template>

<script setup>
/**
 * API search example with request cancellation
 * 
 * @param {String} searchValue - Search query
 * @param {Object} params - Additional request params
 * @param {String} endpoint - API endpoint
 */
const searchWithCancel = async (searchValue, params, endpoint) => {
  const controller = new AbortController();
  const { signal } = controller;

  const response = await fetch(`https://jsonplaceholder.typicode.com/${endpoint}${searchValue ? `?username=${searchValue}` : ''}`, { signal });
  const data = await response.json();

  return {
    data,
    cancel: controller,
  };
};
</script>

This example demonstrates how to implement request cancellation when a new search is initiated before the previous one completes.

Slots

SlotPropsDescription
prefixIcon{ icon }Custom left icon
icon{ icon }Custom right icon
spinner{ spinning }Custom loading indicator
clear-Custom clear button
chip{ optionText, option, removeOption }Custom chip for multiple selection
option{ option, isActive }Custom option rendering
optionSuffix{ option, isActive }Additional option metadata
optionPrefix{ option, isActive }Prefix metadata for options

Improved Documentation

We are planning to provide more examples and advanced use cases for the Combobox component. Stay tuned!

CSS Customization ⚡️

To customize the combobox styles global

css
:root {
  /* Available variables */
  --rvc-combobox-border-radius: var(--rvc-base-border-radius);
  --rvc-combobox-border-width: var(--rvc-base-border-width);
  --rvc-combobox-border-color: var(--rvc-base-border-color);
  --rvc-combobox-font-size: var(--rvc-base-input-font-size);
  --rvc-combobox-font-weight: var(--rvc-base-input-font-weight);
  --rvc-combobox-box-shadow: var(--rvc-base-box-shadow);
  --rvc-combobox-color: var(--rvc-base-input-color);
  --rvc-combobox-bg-color: var(--rvc-base-input-bg-color);
  --rvc-combobox-bg-color-disabled: var(--rvc-base-input-bg-color-disabled);
  --rvc-combobox-disabled-opacity: var(--rvc-base-input-disabled-opacity);
  --rvc-combobox-padding-x: var(--rvc-base-input-padding-x);
  --rvc-combobox-height: var(--rvc-base-input-height);
  --rvc-combobox-icon-size: var(--rvc-base-input-icon-size);
  --rvc-combobox-icon-color: var(--rvc-base-input-icon-color);
  --rvc-combobox-icon-loading-animation: var(--rvc-base-loading-animation);
  --rvc-combobox-placeholder-color: var(--rvc-base-input-placeholder-color);
  --rvc-combobox-clear-color: var(--rvc-base-input-icon-color);
  --rvc-combobox-clear-color-hover: var(--color-slate-700);
  --rvc-combobox-chip-icon-size: calc(var(--spacing) * 4);
  --rvc-combobox-chip-color: var(--rvc-base-input-color);
  --rvc-combobox-chip-color-hover: var(--color-slate-700);
  --rvc-combobox-chips-spacing: calc(var(--spacing) * 2);
  --rvc-combobox-options-offset: calc(var(--spacing) * 1);
  --rvc-combobox-options-z-index: 10;
  --rvc-combobox-options-max-height: calc(var(--spacing) * 60);
  --rvc-combobox-options-padding-x: 0.1875rem;
  --rvc-combobox-options-padding-y: 0.1875rem;
  --rvc-combobox-option-padding-x: calc(var(--spacing) * 2);
  --rvc-combobox-option-padding-y: calc(var(--spacing) * 1.5);
  --rvc-combobox-option-bg-color-hover: var(--color-slate-100);
  --rvc-combobox-option-bg-color-active: transparent;
  --rvc-combobox-option-color-active: var(--rvc-base-input-color);
  --rvc-combobox-option-font-weight-active: var(--font-weight-semibold);

  /* Small variant */
  --rvc-combobox-height-sm: 1.875rem;
  --rvc-combobox-font-size-sm: calc(var(--rvc-base-input-font-size) * 0.9);
  --rvc-combobox-padding-x-sm: calc(var(--rvc-base-input-padding-x) * 0.85);
  --rvc-combobox-icon-size-sm: calc(var(--rvc-base-input-icon-size) * 0.85);
}

Roadmap

  • Add more examples and advanced use cases
  • Improve Docs with more detailed explanations
  • Implement infinite loading for large datasets