Combobox Component
The Combobox component provides a powerful searchable select input with autocomplete functionality built with Vue 3 and Headless UI. It supports local options, 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.
yarn add @robuust-digital/vue-components @vueuse/core @headlessui/vueImport the component from the combobox package
import { Combobox } from '@robuust-digital/vue-components/combobox';Import CSS
@import "@robuust-digital/vue-components/combobox/css";Basic Usage
<template>
<Combobox
v-model="single"
id="basic-example"
:on-search="mockSearch"
placeholder="Search fruits..."
/>
</template>Local Options
Pass an options array to enable local mode. The full list is shown when the combobox receives focus, and typing filters the options using their optionText. No endpoint or onSearch callback is required.
<template>
<Combobox
v-model="local"
id="local-example"
:options="mockItems"
placeholder="Search fruits..."
/>
</template>When both options and onSearch are supplied, local options take precedence. The minLength, debounce, minLoadingTime, endpoint, and cancellation props only affect remote searches.
API Integration Example
Real-world example using JSONPlaceholder API to search users:
<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
| Prop | Type | Default | Description |
|---|---|---|---|
| id | String | Required | Unique identifier for the combobox |
| modelValue | Any | null | v-model binding value |
| options | Array | undefined | Options to display and filter locally |
| endpoint | String | undefined | API endpoint for search requests |
| requestParams | Object | {} | Additional params for API requests |
| onSearch | Function | () => Promise.resolve({ data: [] }) | Callback used for remote searches |
| manualInput | Boolean | false | Allow manual input without selection |
| responseData | Function | data => data | Transform API response data |
| displayValue | Function | item => item?.name | Format selected value display |
| optionText | Function | item => item?.name | Format option text in dropdown |
| disabled | Boolean | false | Disable the combobox |
| minLength | Number | 2 | Minimum characters before search |
| itemKey | String | 'id' | Unique key for options |
| clearable | Boolean | false | Show clear button |
| multiple | Boolean | false | Enable multiple selection |
| searchOnly | Boolean | false | Search-only mode without selection |
| icon | Object, Function | null | Custom icon component |
| prefixIcon | Object, Function | null | Custom left icon component |
| size | String | 'base' | Size variant ('sm' or 'base') |
| debounce | Number | 150 | Debounce time for search in milliseconds |
| minLoadingTime | Number | 0 | Minimum loading indicator display time |
| onCancel | Function | null | Callback to cancel pending requests |
Events
| Event | Payload | Description |
|---|---|---|
| update:modelValue | Any | Emitted when selection changes |
| update:requestParams | Object | Emitted when request params change |
| combobox:noResults | String | Emitted when search returns no results |
| combobox:error | Object, String | Emitted when search encounters an error |
Advanced Examples
Multiple Selection with Custom Display
<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
<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:
<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
| Slot | Props | Description |
|---|---|---|
| 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
: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