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.
yarn add @robuust-digital/vue-components @vueuse/core @headlessui/vue
Import 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>
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 |
endpoint | String | undefined | API endpoint for search requests |
requestParams | Object | {} | Additional params for API requests |
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