Combobox
This library is a work-in-progress. We are releasing it early to gather feedback, but it is not ready for production.
An accessible autocomplete widget that combines a text input with a filterable dropdown list. Perfect for searchable select inputs with keyboard navigation support.
Examples
Live Preview
Basic Combobox
Selected: None
Sizes
Filter Modes
Clearable
Selected: ca
States
With Help Text
Loading State
With Disabled Options
Custom No Results Text
Hidden Label
Monochrome Variant (Single Select)
Multiple Select
Selected:
Multiple with Defaults
Multiple Clearable
Multiple with Max Visible Tags
Multiple Monochrome
Event Handling
External Label Support
The Combobox component supports external labels with helper text, required fields, and validation states.
Label Positioning
Control label position with label-position: 'top' (default), 'start', 'end', or 'bottom'. Note: 'bottom' is not recommended as the dropdown will cover the label when opened.
View Vue Code
<template>
<section>
<div class="mbe4">
<h2>Basic Combobox</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="selectedState"
:options="stateOptions"
label="Select State"
placeholder="Choose a state..."
class="mbe2"
@change="handleStateChange"
/>
<p class="text-sm text-secondary">Selected: {{ selectedState || 'None' }}</p>
</div>
<div class="mbe4">
<h2>Sizes</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="sizeSmall"
:options="stateOptions"
label="Small Combobox"
size="small"
placeholder="Small size"
class="mbe2"
/>
<VueCombobox
v-model:value="sizeDefault"
:options="stateOptions"
label="Default Combobox"
size="default"
placeholder="Default size"
class="mbe2"
/>
<VueCombobox
v-model:value="sizeLarge"
:options="stateOptions"
label="Large Combobox"
size="large"
placeholder="Large size"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Filter Modes</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="filterStartsWith"
:options="stateOptions"
label="Starts With Filter"
filter-mode="startsWith"
placeholder="Type to filter..."
help-text="Try typing 'Ca' to see California"
class="mbe2"
/>
<VueCombobox
v-model:value="filterContains"
:options="stateOptions"
label="Contains Filter"
filter-mode="contains"
placeholder="Type to search..."
help-text="Try typing 'or' to see multiple matches"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Clearable</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="clearableValue"
:options="stateOptions"
label="Clearable Combobox"
:clearable="true"
placeholder="Select and clear..."
help-text="Clear button appears when a value is selected"
class="mbe2"
/>
<p class="text-sm text-secondary">Selected: {{ clearableValue || 'None' }}</p>
</div>
<div class="mbe4">
<h2>States</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="stateDisabled"
:options="stateOptions"
label="Disabled Combobox"
:disabled="true"
placeholder="This is disabled"
class="mbe2"
/>
<VueCombobox
v-model:value="stateReadonly"
:options="stateOptions"
label="Readonly Combobox"
:readonly="true"
placeholder="This is readonly"
class="mbe2"
/>
<VueCombobox
v-model:value="stateRequired"
:options="stateOptions"
label="Required Combobox"
:required="true"
placeholder="This is required"
help-text="This field is required"
class="mbe2"
/>
<VueCombobox
v-model:value="stateInvalid"
:options="stateOptions"
label="Invalid Combobox"
:invalid="true"
error-message="Please select a valid state"
placeholder="This has an error"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>With Help Text</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="helpTextValue"
:options="stateOptions"
label="State of Residence"
placeholder="Select your state..."
help-text="Choose the state where you currently reside"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Loading State</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="loadingValue"
:options="loadingOptions"
label="Loading Combobox"
:loading="isLoading"
loading-text="Fetching states..."
placeholder="Loading..."
class="mbe2"
/>
<button
@click="toggleLoading"
class="btn btn-primary"
>
{{ isLoading ? 'Stop Loading' : 'Start Loading' }}
</button>
</div>
<div class="mbe4">
<h2>With Disabled Options</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="disabledOptionsValue"
:options="optionsWithDisabled"
label="Select State"
placeholder="Some options are disabled..."
help-text="Colorado and Florida are disabled"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Custom No Results Text</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="customNoResults"
:options="stateOptions"
label="Search States"
placeholder="Type to search..."
no-results-text="No states match your search"
help-text="Try typing 'xyz' to see custom no results message"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Hidden Label</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="hiddenLabelValue"
:options="stateOptions"
label="State (Hidden)"
:label-hidden="true"
placeholder="Label is visually hidden but accessible"
help-text="The label is hidden visually but still accessible to screen readers"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Monochrome Variant (Single Select)</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="monochromeValue"
:options="stateOptions"
label="Select State"
variant="monochrome"
placeholder="Choose a state..."
help-text="Monochrome variant with inverted colors for selected items"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Multiple Select</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="multipleValue"
:options="stateOptions"
label="Select States"
:multiple="true"
:close-on-select="false"
placeholder="Choose one or more states..."
help-text="Select multiple states with checkboxes"
class="mbe2"
/>
<p class="text-sm text-secondary">Selected: {{ Array.isArray(multipleValue) ? multipleValue.join(', ') : 'None' }}</p>
</div>
<div class="mbe4">
<h2>Multiple with Defaults</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
:options="stateOptions"
label="Pre-selected States"
:multiple="true"
:close-on-select="false"
:default-value="['ca', 'fl']"
help-text="California and Florida are pre-selected"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Multiple Clearable</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="multipleClearable"
:options="stateOptions"
label="Select States"
:multiple="true"
:clearable="true"
:close-on-select="false"
placeholder="Select and clear multiple..."
help-text="Clear button removes all selections"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Multiple with Max Visible Tags</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
:options="stateOptions"
label="Select States"
:multiple="true"
:close-on-select="false"
:default-value="['al', 'ak', 'az', 'ar', 'ca']"
:max-options-visible="2"
help-text="Shows only 2 tags, with a +3 summary"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Multiple Monochrome</h2>
</div>
<div class="stacked mbe4">
<VueCombobox
v-model:value="multipleMonochrome"
:options="stateOptions"
label="Select States"
:multiple="true"
variant="monochrome"
:close-on-select="false"
placeholder="Choose one or more states..."
help-text="Multiple select with monochrome variant"
class="mbe2"
/>
</div>
<div class="mbe4">
<h2>Event Handling</h2>
</div>
<div class="stacked mbe4">
<div
class="flex-inline items-center"
:style="{gap: '10px'}"
>
<VueCombobox
v-model:value="eventTestValue"
:options="stateOptions"
label="Event Test"
placeholder="Type or select..."
@change="handleChange"
@select="handleSelect"
@search="handleSearch"
class="mbe2"
/>
<p v-if="lastEvent">
Last event: <strong>{{ lastEvent }}</strong>
</p>
</div>
</div>
<div class="mbe4">
<h2>External Label Support</h2>
<p class="mbs2 mbe3">
The Combobox component supports external labels with helper text, required fields, and validation states.
</p>
</div>
<div
class="mbe4"
style="max-width: 600px;"
>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Favorite State"
name="favorite"
placeholder="Choose a state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Home State"
help-text="Select the state where you currently reside"
name="home"
placeholder="Choose your home state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Required State"
:required="true"
help-text="This field is required"
name="required"
placeholder="Choose a state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="State Selection"
:required="true"
:invalid="true"
error-message="Please select a valid state to continue"
name="validation"
placeholder="Choose a state..."
/>
</div>
</div>
<div class="mbe4">
<h2>Label Positioning</h2>
<p class="mbs2 mbe3">
Control label position with <code>label-position</code>: 'top' (default), 'start', 'end', or 'bottom'.
<strong>Note:</strong> 'bottom' is not recommended as the dropdown will cover the label when opened.
</p>
</div>
<div
class="mbe4"
style="max-width: 600px;"
>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Top Label (Default)"
label-position="top"
name="pos-top"
placeholder="Choose a state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Start Position"
label-position="start"
name="pos-start"
placeholder="Choose a state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="End Position"
label-position="end"
name="pos-end"
placeholder="Choose a state..."
/>
</div>
<div class="mbe3">
<VueCombobox
:options="stateOptions"
label="Bottom Position (Not Recommended)"
label-position="bottom"
name="pos-bottom"
help-text="The dropdown will cover this label when opened"
placeholder="Choose a state..."
/>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { ref } from "vue";
import VueCombobox from "agnosticui-core/combobox/vue";
import type { ComboboxOption } from "agnosticui-core/combobox";
// State options
const stateOptions: ComboboxOption[] = [
{ value: "al", label: "Alabama" },
{ value: "ak", label: "Alaska" },
{ value: "az", label: "Arizona" },
{ value: "ar", label: "Arkansas" },
{ value: "ca", label: "California" },
{ value: "co", label: "Colorado" },
{ value: "ct", label: "Connecticut" },
{ value: "de", label: "Delaware" },
{ value: "fl", label: "Florida" },
{ value: "ga", label: "Georgia" },
];
// Options with some disabled
const optionsWithDisabled: ComboboxOption[] = [
{ value: "ca", label: "California" },
{ value: "co", label: "Colorado", disabled: true },
{ value: "ct", label: "Connecticut" },
{ value: "fl", label: "Florida", disabled: true },
{ value: "ny", label: "New York" },
];
// Basic
const selectedState = ref("");
// Sizes
const sizeSmall = ref("");
const sizeDefault = ref("");
const sizeLarge = ref("");
// Filter modes
const filterStartsWith = ref("");
const filterContains = ref("");
// Clearable
const clearableValue = ref("ca");
// States
const stateDisabled = ref("ca");
const stateReadonly = ref("co");
const stateRequired = ref("");
const stateInvalid = ref("");
// Help text
const helpTextValue = ref("");
// Loading
const loadingValue = ref("");
const loadingOptions = ref<ComboboxOption[]>([]);
const isLoading = ref(false);
// Disabled options
const disabledOptionsValue = ref("");
// Custom no results
const customNoResults = ref("");
// Hidden label
const hiddenLabelValue = ref("");
// Monochrome
const monochromeValue = ref("");
// Multiple select
const multipleValue = ref<string[]>([]);
const multipleClearable = ref<string[]>(["ny", "co"]);
const multipleMonochrome = ref<string[]>([]);
// Event handling
const eventTestValue = ref("");
const lastEvent = ref<string | null>(null);
const lastSelectedValue = ref<string | null>(null);
const handleChange = (detail: {
value: string;
option: ComboboxOption | null;
}) => {
lastSelectedValue.value = detail.value;
const optionLabel = detail.option ? detail.option.label : "cleared";
lastEvent.value = `change (value: ${
detail.value || "none"
}, option: ${optionLabel})`;
};
const handleSelect = (detail: { option: ComboboxOption; value: string }) => {
lastSelectedValue.value = detail.value;
lastEvent.value = `select (value: ${detail.value}, label: ${detail.option.label})`;
};
const handleSearch = (detail: { searchTerm: string }) => {
const selectedInfo = lastSelectedValue.value
? `, selected: ${lastSelectedValue.value}`
: "";
lastEvent.value = `search (searchTerm: "${detail.searchTerm}"${selectedInfo})`;
};
// Basic state change handler
const handleStateChange = (detail: {
value: string;
option: ComboboxOption | null;
}) => {
console.log("State changed:", detail);
};
// Toggle loading
const toggleLoading = () => {
isLoading.value = !isLoading.value;
if (isLoading.value) {
loadingOptions.value = [];
// Simulate async loading
setTimeout(() => {
loadingOptions.value = stateOptions;
isLoading.value = false;
}, 2000);
} else {
loadingOptions.value = stateOptions;
}
};
</script>
<style scoped>
.stacked > * + * {
margin-top: 1rem;
}
.text-sm {
font-size: 0.875rem;
}
.text-secondary {
color: var(--ag-text-secondary);
}
.btn {
padding: var(--ag-space-2) var(--ag-space-4);
border-radius: var(--ag-radius-md);
border: none;
cursor: pointer;
font-size: var(--ag-font-size-base);
}
.btn-primary {
background-color: var(--ag-primary);
color: white;
}
.btn-primary:hover {
opacity: 0.9;
}
</style>
Live Preview
View Lit / Web Component Code
import { LitElement, html } from 'lit';
import 'agnosticui-core/combobox';
export class ComboboxLitExamples extends LitElement {
// Render in light DOM to access global utility classes
createRenderRoot() {
return this;
}
constructor() {
super();
// State options
this.stateOptions = [
{ value: 'al', label: 'Alabama' },
{ value: 'ak', label: 'Alaska' },
{ value: 'az', label: 'Arizona' },
{ value: 'ar', label: 'Arkansas' },
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado' },
{ value: 'ct', label: 'Connecticut' },
{ value: 'de', label: 'Delaware' },
{ value: 'fl', label: 'Florida' },
{ value: 'ga', label: 'Georgia' },
];
// Options with some disabled
this.optionsWithDisabled = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado', disabled: true },
{ value: 'ct', label: 'Connecticut' },
{ value: 'fl', label: 'Florida', disabled: true },
{ value: 'ny', label: 'New York' },
];
// State values
this.selectedState = '';
this.isLoading = false;
this.lastEvent = null;
}
firstUpdated() {
// Set options on combobox elements
const comboboxes = this.querySelectorAll('ag-combobox');
comboboxes.forEach((combobox) => {
const comboId = combobox.id;
// Set appropriate options based on combobox ID
if (comboId === 'disabled-options') {
combobox.options = this.optionsWithDisabled;
} else {
combobox.options = this.stateOptions;
}
// Add event listeners
combobox.addEventListener('change', (e) => {
if (comboId === 'basic-combobox') {
this.selectedState = e.detail.value;
this.requestUpdate();
}
});
});
}
toggleLoading() {
this.isLoading = !this.isLoading;
const loadingCombobox = this.querySelector('#loading-combobox');
if (loadingCombobox) {
if (this.isLoading) {
loadingCombobox.options = [];
setTimeout(() => {
loadingCombobox.options = this.stateOptions;
this.isLoading = false;
this.requestUpdate();
}, 2000);
} else {
loadingCombobox.options = this.stateOptions;
}
}
this.requestUpdate();
}
render() {
return html`
<section>
<!-- Basic Combobox -->
<div class="mbe4">
<h2>Basic Combobox</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
id="basic-combobox"
label="Select State"
placeholder="Choose a state..."
class="mbe2"
></ag-combobox>
<p class="text-sm text-secondary">Selected: ${this.selectedState || 'None'}</p>
</div>
<!-- Sizes -->
<div class="mbe4">
<h2>Sizes</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Small Combobox"
size="small"
placeholder="Small size"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Default Combobox"
size="default"
placeholder="Default size"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Large Combobox"
size="large"
placeholder="Large size"
class="mbe2"
></ag-combobox>
</div>
<!-- Filter Modes -->
<div class="mbe4">
<h2>Filter Modes</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Starts With Filter"
filter-mode="startsWith"
placeholder="Type to filter..."
help-text="Try typing 'Ca' to see California"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Contains Filter"
filter-mode="contains"
placeholder="Type to search..."
help-text="Try typing 'or' to see multiple matches"
class="mbe2"
></ag-combobox>
</div>
<!-- Clearable -->
<div class="mbe4">
<h2>Clearable</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Clearable Combobox"
clearable
placeholder="Select and clear..."
help-text="Clear button appears when a value is selected"
value="ca"
class="mbe2"
></ag-combobox>
</div>
<!-- States -->
<div class="mbe4">
<h2>States</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Disabled Combobox"
disabled
placeholder="This is disabled"
value="ca"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Readonly Combobox"
readonly
placeholder="This is readonly"
value="co"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Required Combobox"
required
placeholder="This is required"
help-text="This field is required"
class="mbe2"
></ag-combobox>
<ag-combobox
label="Invalid Combobox"
invalid
error-message="Please select a valid state"
placeholder="This has an error"
class="mbe2"
></ag-combobox>
</div>
<!-- With Help Text -->
<div class="mbe4">
<h2>With Help Text</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="State of Residence"
placeholder="Select your state..."
help-text="Choose the state where you currently reside"
class="mbe2"
></ag-combobox>
</div>
<!-- Loading State -->
<div class="mbe4">
<h2>Loading State</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
id="loading-combobox"
label="Loading Combobox"
?loading=${this.isLoading}
loading-text="Fetching states..."
placeholder="Loading..."
class="mbe2"
></ag-combobox>
<button
@click=${this.toggleLoading}
class="btn btn-primary"
>
${this.isLoading ? 'Stop Loading' : 'Start Loading'}
</button>
</div>
<!-- With Disabled Options -->
<div class="mbe4">
<h2>With Disabled Options</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
id="disabled-options"
label="Select State"
placeholder="Some options are disabled..."
help-text="Colorado and Florida are disabled"
class="mbe2"
></ag-combobox>
</div>
<!-- Custom No Results Text -->
<div class="mbe4">
<h2>Custom No Results Text</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Search States"
placeholder="Type to search..."
no-results-text="No states match your search"
help-text="Try typing 'xyz' to see custom no results message"
class="mbe2"
></ag-combobox>
</div>
<!-- Hidden Label -->
<div class="mbe4">
<h2>Hidden Label</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="State (Hidden)"
label-hidden
placeholder="Label is visually hidden but accessible"
help-text="The label is hidden visually but still accessible to screen readers"
class="mbe2"
></ag-combobox>
</div>
<!-- Monochrome Variant -->
<div class="mbe4">
<h2>Monochrome Variant (Single Select)</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Select State"
variant="monochrome"
placeholder="Choose a state..."
help-text="Monochrome variant with inverted colors for selected items"
class="mbe2"
></ag-combobox>
</div>
<!-- Multiple Select -->
<div class="mbe4">
<h2>Multiple Select</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Select States"
multiple
placeholder="Choose one or more states..."
help-text="Select multiple states with checkboxes"
class="mbe2"
></ag-combobox>
</div>
<!-- Multiple with Defaults -->
<div class="mbe4">
<h2>Multiple with Defaults</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Pre-selected States"
multiple
default-value='["ca", "fl"]'
help-text="California and Florida are pre-selected"
class="mbe2"
></ag-combobox>
</div>
<!-- Multiple Clearable -->
<div class="mbe4">
<h2>Multiple Clearable</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Select States"
multiple
clearable
placeholder="Select and clear multiple..."
help-text="Clear button removes all selections"
default-value='["ny", "co"]'
class="mbe2"
></ag-combobox>
</div>
<!-- Multiple with Max Visible Tags -->
<div class="mbe4">
<h2>Multiple with Max Visible Tags</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Select States"
multiple
default-value='["al", "ak", "az", "ar", "ca"]'
max-options-visible="2"
help-text="Shows only 2 tags, with a +3 summary"
class="mbe2"
></ag-combobox>
</div>
<!-- Multiple Monochrome -->
<div class="mbe4">
<h2>Multiple Monochrome</h2>
</div>
<div class="stacked mbe4">
<ag-combobox
label="Select States"
multiple
variant="monochrome"
placeholder="Choose one or more states..."
help-text="Multiple select with monochrome variant"
class="mbe2"
></ag-combobox>
</div>
<!-- External Label Support -->
<div class="mbe4">
<h2>External Label Support</h2>
<p class="mbs2 mbe3">
The Combobox component supports external labels with helper text, required fields, and validation states.
</p>
</div>
<div class="mbe4" style="max-width: 600px;">
<div class="mbe3">
<ag-combobox
label="Favorite State"
name="favorite"
placeholder="Choose a state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="Home State"
help-text="Select the state where you currently reside"
name="home"
placeholder="Choose your home state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="Required State"
required
help-text="This field is required"
name="required"
placeholder="Choose a state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="State Selection"
required
invalid
error-message="Please select a valid state to continue"
name="validation"
placeholder="Choose a state..."
></ag-combobox>
</div>
</div>
<!-- Label Positioning -->
<div class="mbe4">
<h2>Label Positioning</h2>
<p class="mbs2 mbe3">
Control label position with <code>label-position</code>: 'top' (default), 'start', 'end', or 'bottom'.
<strong>Note:</strong> 'bottom' is not recommended as the dropdown will cover the label when opened.
</p>
</div>
<div class="mbe4" style="max-width: 600px;">
<div class="mbe3">
<ag-combobox
label="Top Label (Default)"
label-position="top"
name="pos-top"
placeholder="Choose a state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="Start Position"
label-position="start"
name="pos-start"
placeholder="Choose a state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="End Position"
label-position="end"
name="pos-end"
placeholder="Choose a state..."
></ag-combobox>
</div>
<div class="mbe3">
<ag-combobox
label="Bottom Position (Not Recommended)"
label-position="bottom"
name="pos-bottom"
help-text="The dropdown will cover this label when opened"
placeholder="Choose a state..."
></ag-combobox>
</div>
</div>
</section>
`;
}
}
// Register the custom element
customElements.define('combobox-lit-examples', ComboboxLitExamples);
Interactive Preview: Click the "Open in StackBlitz" button below to see this example running live in an interactive playground.
View React Code
import { useState } from "react";
import { ReactCombobox } from "agnosticui-core/combobox/react";
export default function ComboboxReactExamples() {
// State options
const stateOptions = [
{ value: "al", label: "Alabama" },
{ value: "ak", label: "Alaska" },
{ value: "az", label: "Arizona" },
{ value: "ar", label: "Arkansas" },
{ value: "ca", label: "California" },
{ value: "co", label: "Colorado" },
{ value: "ct", label: "Connecticut" },
{ value: "de", label: "Delaware" },
{ value: "fl", label: "Florida" },
{ value: "ga", label: "Georgia" },
];
// Options with some disabled
const optionsWithDisabled = [
{ value: "ca", label: "California" },
{ value: "co", label: "Colorado", disabled: true },
{ value: "ct", label: "Connecticut" },
{ value: "fl", label: "Florida", disabled: true },
{ value: "ny", label: "New York" },
];
// State values
const [selectedState, setSelectedState] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [loadingOptions, setLoadingOptions] = useState([]);
const [lastEvent, setLastEvent] = useState(null);
const handleStateChange = (detail) => {
setSelectedState(detail.value);
console.log("State changed:", detail);
};
const toggleLoading = () => {
setIsLoading(!isLoading);
if (!isLoading) {
setLoadingOptions([]);
// Simulate async loading
setTimeout(() => {
setLoadingOptions(stateOptions);
setIsLoading(false);
}, 2000);
} else {
setLoadingOptions(stateOptions);
}
};
const handleChange = (detail) => {
const optionLabel = detail.option ? detail.option.label : "cleared";
setLastEvent(`change (value: ${detail.value || "none"}, option: ${optionLabel})`);
};
const handleSelect = (detail) => {
setLastEvent(`select (value: ${detail.value}, label: ${detail.option.label})`);
};
const handleSearch = (detail) => {
setLastEvent(`search (searchTerm: "${detail.searchTerm}")`);
};
return (
<section>
{/* Basic Combobox */}
<div className="mbe4">
<h2>Basic Combobox</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select State"
placeholder="Choose a state..."
className="mbe2"
onChange={handleStateChange}
/>
<p className="text-sm text-secondary">Selected: {selectedState || 'None'}</p>
</div>
{/* Sizes */}
<div className="mbe4">
<h2>Sizes</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Small Combobox"
size="small"
placeholder="Small size"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Default Combobox"
size="default"
placeholder="Default size"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Large Combobox"
size="large"
placeholder="Large size"
className="mbe2"
/>
</div>
{/* Filter Modes */}
<div className="mbe4">
<h2>Filter Modes</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Starts With Filter"
filterMode="startsWith"
placeholder="Type to filter..."
helpText="Try typing 'Ca' to see California"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Contains Filter"
filterMode="contains"
placeholder="Type to search..."
helpText="Try typing 'or' to see multiple matches"
className="mbe2"
/>
</div>
{/* Clearable */}
<div className="mbe4">
<h2>Clearable</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Clearable Combobox"
clearable
placeholder="Select and clear..."
helpText="Clear button appears when a value is selected"
value="ca"
className="mbe2"
/>
</div>
{/* States */}
<div className="mbe4">
<h2>States</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Disabled Combobox"
disabled
placeholder="This is disabled"
value="ca"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Readonly Combobox"
readonly
placeholder="This is readonly"
value="co"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Required Combobox"
required
placeholder="This is required"
helpText="This field is required"
className="mbe2"
/>
<ReactCombobox
options={stateOptions}
label="Invalid Combobox"
invalid
errorMessage="Please select a valid state"
placeholder="This has an error"
className="mbe2"
/>
</div>
{/* With Help Text */}
<div className="mbe4">
<h2>With Help Text</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="State of Residence"
placeholder="Select your state..."
helpText="Choose the state where you currently reside"
className="mbe2"
/>
</div>
{/* Loading State */}
<div className="mbe4">
<h2>Loading State</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={loadingOptions}
label="Loading Combobox"
loading={isLoading}
loadingText="Fetching states..."
placeholder="Loading..."
className="mbe2"
/>
<button
onClick={toggleLoading}
className="btn btn-primary"
>
{isLoading ? 'Stop Loading' : 'Start Loading'}
</button>
</div>
{/* With Disabled Options */}
<div className="mbe4">
<h2>With Disabled Options</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={optionsWithDisabled}
label="Select State"
placeholder="Some options are disabled..."
helpText="Colorado and Florida are disabled"
className="mbe2"
/>
</div>
{/* Custom No Results Text */}
<div className="mbe4">
<h2>Custom No Results Text</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Search States"
placeholder="Type to search..."
noResultsText="No states match your search"
helpText="Try typing 'xyz' to see custom no results message"
className="mbe2"
/>
</div>
{/* Hidden Label */}
<div className="mbe4">
<h2>Hidden Label</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="State (Hidden)"
labelHidden
placeholder="Label is visually hidden but accessible"
helpText="The label is hidden visually but still accessible to screen readers"
className="mbe2"
/>
</div>
{/* Monochrome Variant */}
<div className="mbe4">
<h2>Monochrome Variant (Single Select)</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select State"
variant="monochrome"
placeholder="Choose a state..."
helpText="Monochrome variant with inverted colors for selected items"
className="mbe2"
/>
</div>
{/* Multiple Select */}
<div className="mbe4">
<h2>Multiple Select</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select States"
multiple
closeOnSelect={false}
placeholder="Choose one or more states..."
helpText="Select multiple states with checkboxes"
className="mbe2"
/>
</div>
{/* Multiple with Defaults */}
<div className="mbe4">
<h2>Multiple with Defaults</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Pre-selected States"
multiple
closeOnSelect={false}
defaultValue={["ca", "fl"]}
helpText="California and Florida are pre-selected"
className="mbe2"
/>
</div>
{/* Multiple Clearable */}
<div className="mbe4">
<h2>Multiple Clearable</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select States"
multiple
clearable
closeOnSelect={false}
defaultValue={["ny", "co"]}
placeholder="Select and clear multiple..."
helpText="Clear button removes all selections"
className="mbe2"
/>
</div>
{/* Multiple with Max Visible Tags */}
<div className="mbe4">
<h2>Multiple with Max Visible Tags</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select States"
multiple
closeOnSelect={false}
defaultValue={["al", "ak", "az", "ar", "ca"]}
maxOptionsVisible={2}
helpText="Shows only 2 tags, with a +3 summary"
className="mbe2"
/>
</div>
{/* Multiple Monochrome */}
<div className="mbe4">
<h2>Multiple Monochrome</h2>
</div>
<div className="stacked mbe4">
<ReactCombobox
options={stateOptions}
label="Select States"
multiple
variant="monochrome"
closeOnSelect={false}
placeholder="Choose one or more states..."
helpText="Multiple select with monochrome variant"
className="mbe2"
/>
</div>
{/* Event Handling */}
<div className="mbe4">
<h2>Event Handling</h2>
</div>
<div className="stacked mbe4">
<div className="flex-inline items-center" style={{ gap: "10px" }}>
<ReactCombobox
options={stateOptions}
label="Event Test"
placeholder="Type or select..."
onChange={handleChange}
onSelect={handleSelect}
onSearch={handleSearch}
className="mbe2"
/>
{lastEvent && (
<p>
Last event: <strong>{lastEvent}</strong>
</p>
)}
</div>
</div>
{/* External Label Support */}
<div className="mbe4">
<h2>External Label Support</h2>
<p className="mbs2 mbe3">
The Combobox component supports external labels with helper text, required fields, and validation states.
</p>
</div>
<div className="mbe4" style={{ maxWidth: "600px" }}>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Favorite State"
name="favorite"
placeholder="Choose a state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Home State"
helpText="Select the state where you currently reside"
name="home"
placeholder="Choose your home state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Required State"
required
helpText="This field is required"
name="required"
placeholder="Choose a state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="State Selection"
required
invalid
errorMessage="Please select a valid state to continue"
name="validation"
placeholder="Choose a state..."
/>
</div>
</div>
{/* Label Positioning */}
<div className="mbe4">
<h2>Label Positioning</h2>
<p className="mbs2 mbe3">
Control label position with <code>labelPosition</code>: 'top' (default), 'start', 'end', or 'bottom'.
<strong>Note:</strong> 'bottom' is not recommended as the dropdown will cover the label when opened.
</p>
</div>
<div className="mbe4" style={{ maxWidth: "600px" }}>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Top Label (Default)"
labelPosition="top"
name="pos-top"
placeholder="Choose a state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Start Position"
labelPosition="start"
name="pos-start"
placeholder="Choose a state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="End Position"
labelPosition="end"
name="pos-end"
placeholder="Choose a state..."
/>
</div>
<div className="mbe3">
<ReactCombobox
options={stateOptions}
label="Bottom Position (Not Recommended)"
labelPosition="bottom"
name="pos-bottom"
helpText="The dropdown will cover this label when opened"
placeholder="Choose a state..."
/>
</div>
</div>
</section>
);
}
Usage
TIP
The framework examples below import AgnosticUI as an npm package. Alternatively, you can use the CLI for complete control, AI/LLM visibility, and full code ownership:
npx ag init --framework FRAMEWORK # react, vue, lit, svelte, etc.
npx ag add ComboboxThe CLI copies source code directly into your project, giving you full visibility and control. After running npx ag add, you'll receive exact import instructions.
Vue
<template>
<section>
<VueCombobox
v-model:value="selectedState"
:options="stateOptions"
label="Select State"
placeholder="Choose a state..."
@change="handleChange"
/>
<VueCombobox
v-model:value="state"
:options="stateOptions"
label="State of Residence"
placeholder="Select your state..."
help-text="Choose the state where you currently reside"
/>
<VueCombobox
v-model:value="requiredState"
:options="stateOptions"
label="Required State"
:required="true"
:invalid="isInvalid"
error-message="State is required"
/>
<VueCombobox
v-model:value="clearableState"
:options="stateOptions"
label="Select State"
:clearable="true"
placeholder="Select and clear..."
/>
<VueCombobox
v-model:value="searchState"
:options="stateOptions"
label="Search States"
filter-mode="contains"
placeholder="Type to search..."
@search="handleSearch"
/>
<VueCombobox
v-model:value="small"
:options="stateOptions"
label="Small Combobox"
size="small"
/>
<VueCombobox
v-model:value="large"
:options="stateOptions"
label="Large Combobox"
size="large"
/>
<VueCombobox
v-model:value="value"
:options="optionsWithDisabled"
label="Select State"
placeholder="Some options are disabled..."
/>
<VueCombobox
v-model:value="asyncValue"
:options="asyncOptions"
label="Select State"
:loading="isLoading"
loading-text="Fetching states..."
/>
<VueCombobox
v-model:value="disabled"
:options="stateOptions"
label="Disabled Combobox"
:disabled="true"
/>
</section>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import VueCombobox from 'agnosticui-core/combobox/vue';
import type { ComboboxOption } from 'agnosticui-core/combobox';
const stateOptions: ComboboxOption[] = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado' },
{ value: 'ct', label: 'Connecticut' },
{ value: 'fl', label: 'Florida' },
{ value: 'ny', label: 'New York' },
];
const optionsWithDisabled: ComboboxOption[] = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado', disabled: true },
{ value: 'ct', label: 'Connecticut' },
];
const selectedState = ref('');
const state = ref('');
const requiredState = ref('');
const isInvalid = ref(false);
const clearableState = ref('ca');
const searchState = ref('');
const small = ref('');
const large = ref('');
const value = ref('');
const asyncValue = ref('');
const asyncOptions = ref<ComboboxOption[]>([]);
const isLoading = ref(false);
const disabled = ref('ca');
const handleChange = (event: CustomEvent) => {
console.log('Selected:', event.detail.value);
};
const handleSearch = (event: CustomEvent) => {
console.log('Search term:', event.detail.searchTerm);
};
</script>React
import { useState } from 'react';
import { ReactCombobox } from 'agnosticui-core/combobox/react';
import type { ComboboxOption, ComboboxChangeEvent } from 'agnosticui-core/combobox/react';
export default function ComboboxExample() {
const stateOptions: ComboboxOption[] = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado' },
{ value: 'ct', label: 'Connecticut' },
{ value: 'fl', label: 'Florida' },
{ value: 'ny', label: 'New York' },
];
const optionsWithDisabled: ComboboxOption[] = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado', disabled: true },
{ value: 'ct', label: 'Connecticut' },
];
const [selectedState, setSelectedState] = useState('');
const [state, setState] = useState('');
const [requiredState, setRequiredState] = useState('');
const [isInvalid, setIsInvalid] = useState(false);
const [clearableState, setClearableState] = useState('ca');
const [searchState, setSearchState] = useState('');
const [small, setSmall] = useState('');
const [large, setLarge] = useState('');
const [value, setValue] = useState('');
const [asyncValue, setAsyncValue] = useState('');
const [asyncOptions, setAsyncOptions] = useState<ComboboxOption[]>([]);
const [isLoading, setIsLoading] = useState(false);
const handleChange = (event: ComboboxChangeEvent) => {
console.log('Selected:', event.detail.value);
setSelectedState(event.detail.value);
};
const handleSearch = (event: CustomEvent) => {
console.log('Search term:', event.detail.searchTerm);
};
return (
<section>
<ReactCombobox
value={selectedState}
options={stateOptions}
label="Select State"
placeholder="Choose a state..."
onChange={handleChange}
/>
<ReactCombobox
value={state}
options={stateOptions}
label="State of Residence"
placeholder="Select your state..."
helpText="Choose the state where you currently reside"
onChange={(e) => setState(e.detail.value)}
/>
<ReactCombobox
value={requiredState}
options={stateOptions}
label="Required State"
required
invalid={isInvalid}
errorMessage="State is required"
onChange={(e) => setRequiredState(e.detail.value)}
/>
<ReactCombobox
value={clearableState}
options={stateOptions}
label="Select State"
clearable
placeholder="Select and clear..."
onChange={(e) => setClearableState(e.detail.value)}
/>
<ReactCombobox
value={searchState}
options={stateOptions}
label="Search States"
filterMode="contains"
placeholder="Type to search..."
onChange={(e) => setSearchState(e.detail.value)}
onSearch={handleSearch}
/>
<ReactCombobox
value={small}
options={stateOptions}
label="Small Combobox"
size="small"
onChange={(e) => setSmall(e.detail.value)}
/>
<ReactCombobox
value={large}
options={stateOptions}
label="Large Combobox"
size="large"
onChange={(e) => setLarge(e.detail.value)}
/>
<ReactCombobox
value={value}
options={optionsWithDisabled}
label="Select State"
placeholder="Some options are disabled..."
onChange={(e) => setValue(e.detail.value)}
/>
<ReactCombobox
value={asyncValue}
options={asyncOptions}
label="Select State"
loading={isLoading}
loadingText="Fetching states..."
onChange={(e) => setAsyncValue(e.detail.value)}
/>
<ReactCombobox
value="ca"
options={stateOptions}
label="Disabled Combobox"
disabled
/>
</section>
);
}Lit
import { LitElement, html } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import 'agnosticui-core/combobox';
import type { ComboboxOption, ComboboxChangeEvent } from 'agnosticui-core/combobox';
@customElement('combobox-example')
export class ComboboxExample extends LitElement {
@state()
private stateOptions: ComboboxOption[] = [
{ value: 'ca', label: 'California' },
{ value: 'co', label: 'Colorado' },
{ value: 'ct', label: 'Connecticut' },
{ value: 'fl', label: 'Florida' },
{ value: 'ny', label: 'New York' },
];
@state()
private selectedState = '';
@state()
private clearableState = 'ca';
@state()
private isLoading = false;
private handleChange(event: ComboboxChangeEvent) {
console.log('Selected:', event.detail.value);
this.selectedState = event.detail.value;
}
private handleSearch(event: CustomEvent) {
console.log('Search term:', event.detail.searchTerm);
}
render() {
return html`
<ag-combobox
.options=${this.stateOptions}
.value=${this.selectedState}
label="Select State"
placeholder="Choose a state..."
@change=${this.handleChange}
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
label="State of Residence"
placeholder="Select your state..."
help-text="Choose the state where you currently reside"
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
label="Required State"
?required=${true}
?invalid=${false}
error-message="State is required"
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
.value=${this.clearableState}
label="Select State"
?clearable=${true}
placeholder="Select and clear..."
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
label="Search States"
filter-mode="contains"
placeholder="Type to search..."
@search=${this.handleSearch}
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
label="Small Combobox"
size="small"
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
label="Large Combobox"
size="large"
></ag-combobox>
<ag-combobox
.options=${[]}
label="Select State"
?loading=${this.isLoading}
loading-text="Fetching states..."
></ag-combobox>
<ag-combobox
.options=${this.stateOptions}
.value=${'ca'}
label="Disabled Combobox"
?disabled=${true}
></ag-combobox>
`;
}
}API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | ComboboxOption[] | [] | Array of options to display in the listbox |
value | string | '' | Currently selected value |
defaultValue | string | '' | Initial value (uncontrolled mode) |
placeholder | string | '' | Placeholder text for the input |
label | string | '' | Label text for the combobox |
labelHidden | boolean | false | Visually hide the label (still accessible to screen readers) |
noLabel | boolean | false | Remove the label entirely |
labelPosition | 'top' | 'start' | 'end' | 'bottom' | 'top' | Position of the label relative to the combobox |
ariaLabel | string | null | null | ARIA label for accessibility |
helpText | string | '' | Help text displayed below the input |
errorMessage | string | '' | Error message displayed below the input |
id | string | (auto-generated) | Unique ID for the combobox |
autocomplete | 'list' | 'none' | 'list' | Autocomplete behavior |
filterMode | 'startsWith' | 'contains' | 'none' | 'startsWith' | How to filter options based on input |
clearable | boolean | false | Show clear button when value is selected |
disabled | boolean | false | Disable the combobox |
readonly | boolean | false | Make the combobox read-only |
required | boolean | false | Mark the combobox as required |
invalid | boolean | false | Mark the combobox as invalid |
size | 'small' | 'default' | 'large' | 'default' | Size variant |
maxVisibleOptions | number | undefined | Maximum number of visible options |
closeOnSelect | boolean | true | Close listbox after selecting an option |
loading | boolean | false | Show loading state |
loadingText | string | 'Loading...' | Loading message text |
noResultsText | string | 'No results found' | No results message text |
ComboboxOption Interface
interface ComboboxOption {
value: string;
label: string;
disabled?: boolean;
group?: string;
icon?: string;
description?: string;
metadata?: Record<string, unknown>;
}Events
| Event | Detail | Description |
|---|---|---|
change | { value: string; option: ComboboxOption | null } | Fired when selection changes |
select | { option: ComboboxOption; value: string } | Fired when an option is selected |
search | { searchTerm: string } | Fired when search term changes |
open | { open: boolean } | Fired when listbox opens |
close | { open: boolean } | Fired when listbox closes |
focus | FocusEvent | Fired when input gains focus |
blur | FocusEvent | Fired when input loses focus |
Methods
| Method | Signature | Description |
|---|---|---|
focus() | () => void | Focus the input |
blur() | () => void | Blur the input |
open() | () => void | Open the listbox |
close() | () => void | Close the listbox |
toggle() | () => void | Toggle listbox open/closed |
selectOption() | (optionOrValue: ComboboxOption | string) => void | Select an option by value or object |
clearSelection() | () => void | Clear the current selection |
CSS Parts
Use ::part() to style component internals:
ag-combobox::part(ag-combobox-wrapper) { }
ag-combobox::part(ag-combobox-label) { }
ag-combobox::part(ag-combobox-input-wrapper) { }
ag-combobox::part(ag-combobox-input) { }
ag-combobox::part(ag-combobox-toggle-button) { }
ag-combobox::part(ag-combobox-clear-button) { }
ag-combobox::part(ag-combobox-listbox) { }
ag-combobox::part(ag-combobox-option) { }
ag-combobox::part(ag-combobox-loading) { }
ag-combobox::part(ag-combobox-no-results) { }
ag-combobox::part(ag-combobox-help-text) { }
ag-combobox::part(ag-combobox-error-message) { }Design Tokens
Customize appearance with CSS custom properties:
ag-combobox {
--combobox-min-width: 200px;
--combobox-max-width: 100%;
--combobox-listbox-max-height: 300px;
--combobox-option-padding: var(--ag-space-3) var(--ag-space-4);
--combobox-option-selected-bg: var(--ag-blue-100);
--combobox-option-hover-bg: var(--ag-background-tertiary);
--combobox-option-focus-bg: var(--ag-blue-50);
--combobox-toggle-size: var(--ag-space-6);
}Keyboard Navigation
| Key | Action |
|---|---|
ArrowDown | Opens listbox (if closed) and moves focus to next option |
ArrowUp | Opens listbox (if closed) and moves focus to previous option |
Enter | Selects the focused option and closes listbox |
Escape | Closes listbox (if open) or clears input (if closed with text) |
Tab | Closes listbox and moves focus to next element |
Home | Moves cursor to start of input |
End | Moves cursor to end of input |
| Printable characters | Filters options based on typed text |
Accessibility
The Combobox component follows WCAG 2.1 Level AA guidelines and implements the W3C ARIA APG Combobox Pattern:
- ARIA attributes: Proper
role,aria-expanded,aria-controls,aria-activedescendant,aria-autocomplete - Keyboard navigation: Full keyboard support with arrow keys, Enter, Escape, Tab
- Focus management: Uses
aria-activedescendantpattern for predictable screen reader behavior - Screen reader announcements: Live regions announce options as user navigates
- Focus indicators: Clear 2px outline with 3:1 contrast ratio
- High contrast mode: Tested with Windows High Contrast Mode
- Reduced motion: Respects
prefers-reduced-motionsetting - Color independence: Selection state indicated with visual markers beyond color
Screen Reader Support
Tested with:
- NVDA (Windows)
- JAWS (Windows)
- VoiceOver (macOS, iOS)
- TalkBack (Android)
Filter Modes
startsWith (default)
Matches options that start with the search term. Fast and predictable.
<VueCombobox
:options="options"
filter-mode="startsWith"
/>contains
Matches options that contain the search term anywhere. More forgiving for users.
<VueCombobox
:options="options"
filter-mode="contains"
/>none
Disables client-side filtering. Useful for server-side filtering scenarios.
<VueCombobox
:options="options"
filter-mode="none"
@search="handleServerSearch"
/>Best Practices
When to Use
Use Combobox when:
- Users need to search through many options (>10)
- You want to provide typeahead/autocomplete functionality
- Users might know the exact value they're looking for
- You need accessible keyboard navigation
- You want to allow filtering/searching
Don't use Combobox when:
- You have fewer than 5 simple options (use Radio or Select)
- Users need to compare options visually (use Radio group)
- The selection is critical and requires user confirmation (use Select with explicit submit)
Accessibility Tips
- Always provide a label: Use
labelprop oraria-labelfor screen readers - Use help text: Provide guidance with
helpTextprop - Clear error messages: Use
errorMessagewith specific, actionable messages - Test keyboard navigation: Ensure all features work without a mouse
- Test with screen readers: Verify announcements are clear and helpful
Performance
- Options are filtered in real-time as users type
- Uses efficient
startsWithorcontainsalgorithms - Limits visible options with
maxVisibleOptionsfor large datasets - For very large datasets (>1000), consider server-side filtering with
filter-mode="none"
Phase 2 Features (Future)
The following features are planned for Phase 2:
- Multiselect: Select multiple options with tags
- Grouped options: Organize options into categories
- Rich content: Icons, descriptions, avatars in options
- Async filtering: Built-in debounce and async support
- Fuzzy matching: More forgiving search algorithm
- Virtual scrolling: Handle thousands of options efficiently
- Mobile optimization: Fullscreen mode on small screens