Combobox
An autocomplete input combined with a selectable list. It supports two layout modes — inline input and popup input — filters items as the user types, highlights matching options, and positions its content panel relative to the trigger using floating UI.
import { Combobox } from "@areia/slots";
document.querySelector("#root")!.innerHTML = `
<div data-slot="combobox" class="flex w-72 flex-wrap gap-1">
<input
data-slot="combobox-input"
placeholder="Search..."
class="win95-input min-w-0 flex-1"
/>
<button
data-slot="combobox-trigger"
aria-label="Toggle list"
class="win95-button px-2"
>
▼
</button>
<div data-slot="combobox-content" hidden class="win95-menu w-full">
<div data-slot="combobox-list">
<div data-slot="combobox-empty" hidden class="px-3 py-1 text-neutral-600">
No results found.
</div>
<div data-slot="combobox-group">
<div data-slot="combobox-label" class="win95-label">
Fruits
</div>
<div
data-slot="combobox-item"
data-value="apple"
class="win95-menu-item"
>
Apple
</div>
<div
data-slot="combobox-item"
data-value="banana"
class="win95-menu-item"
>
Banana
</div>
</div>
<div data-slot="combobox-separator" class="win95-separator"></div>
<div data-slot="combobox-item" data-value="other" class="win95-menu-item">
Other
</div>
<div
data-slot="combobox-item"
data-value="disabled"
data-disabled
class="win95-menu-item"
>
Disabled
</div>
</div>
</div>
</div>
`;
const root = document.querySelector('[data-slot="combobox"]')!;
const controller = Combobox.createCombobox(root, {
placeholder: "Search...",
openOnFocus: true,
autoHighlight: true,
});Import
import { Combobox } from "@areia/slots";Usage
import { Combobox } from "@areia/slots";
// Auto-bind a single root
const root = document.querySelector('[data-slot="combobox"]');
const controller = Combobox.createCombobox(root, {
placeholder: "Choose an option...",
onValueChange: (value) => console.log("selected:", value),
onOpenChange: (open) => console.log("open changed:", open),
});
// Auto-bind all unbound roots in scope
const controllers = Combobox.create();Expected Markup
Inline Input Mode
The input lives outside the content panel alongside the trigger button.
<div data-slot="combobox">
<input data-slot="combobox-input" placeholder="Search..." />
<button data-slot="combobox-trigger" aria-label="Toggle list">
▼
</button>
<button
data-slot="combobox-clear"
aria-label="Clear selection"
hidden
>
✕
</button>
<div data-slot="combobox-content" hidden>
<div data-slot="combobox-list">
<div data-slot="combobox-empty" hidden>
No results found.
</div>
<div data-slot="combobox-group">
<div data-slot="combobox-label">Fruits</div>
<div data-slot="combobox-item" data-value="apple">
Apple
</div>
<div data-slot="combobox-item" data-value="banana">
Banana
</div>
</div>
<div data-slot="combobox-separator"></div>
<div data-slot="combobox-item" data-value="other">
Other
</div>
<div
data-slot="combobox-item"
data-value="disabled"
data-disabled
>
Disabled
</div>
</div>
</div>
</div>Popup Input Mode
The input moves inside the content panel and the trigger displays the selected value.
<div data-slot="combobox">
<button data-slot="combobox-trigger">
<span data-slot="combobox-value">Select an option...</span>
</button>
<div data-slot="combobox-content" hidden>
<input data-slot="combobox-input" placeholder="Search..." />
<div data-slot="combobox-list">
<div data-slot="combobox-item" data-value="apple">
Apple
</div>
<div data-slot="combobox-item" data-value="banana">
Banana
</div>
<div data-slot="combobox-item" data-value="cherry">
Cherry
</div>
</div>
</div>
</div>Data Slots
| Slot | Required | Description |
|---|---|---|
combobox |
Yes | Root container. |
combobox-input |
Yes | Text input for typing and filtering. |
combobox-trigger |
No | Button that opens and closes the popup. |
combobox-content |
Yes | Popup panel that contains the list and optionally an input. |
combobox-list |
Yes | Scrollable container for items, groups, separators, and empty slot. |
combobox-item |
Yes | Selectable option. Requires data-value. |
combobox-group |
No | Wraps a set of related items with a label. |
combobox-label |
No | Visible heading inside a group. |
combobox-empty |
No | Message shown when no items match the filter. |
combobox-separator |
No | Visual divider between items or groups. |
combobox-clear |
No | Button that clears the current selection. |
combobox-value |
No | Displays the selected value inside a button trigger (popup-input mode). |
Generated Attributes
| Attribute | Element(s) | Description |
|---|---|---|
data-state |
Root | "open" or "closed". |
data-highlighted |
combobox-item |
Present on the currently highlighted item. |
data-selected |
combobox-item |
Present on the currently selected item. |
data-disabled |
combobox-item |
Present when the item is disabled. |
aria-expanded |
combobox-input, combobox-trigger |
"true" when open, "false" when closed. |
aria-activedescendant |
Root | Points to the highlighted item id when keyboard-navigating. |
role |
combobox-input |
Set to "combobox". |
role |
combobox-list |
Set to "listbox". |
role |
combobox-item |
Set to "option". |
role |
combobox-group |
Set to "group". |
hidden |
combobox-content |
Toggled based on open state. |
Events
Outbound
| Event | Detail | Description |
|---|---|---|
combobox:change |
{ value: string | null, label: string | null, open?: boolean } |
Fired on the root when the selected value changes. |
Inbound
| Event | Detail | Description |
|---|---|---|
combobox:select |
{ value: string } |
Dispatch on the root to programmatically select a value. |
API Reference
Options
| Option | Type | Default | Description |
|---|---|---|---|
defaultValue |
string |
— | Initial selected value. |
defaultOpen |
boolean |
false |
Initial open state. |
placeholder |
string |
— | Placeholder text for the input. |
disabled |
boolean |
false |
Disable interaction. |
required |
boolean |
false |
Mark the field as required for form validation. |
name |
string |
— | Form field name. Creates a hidden input for native form submission. |
openOnFocus |
boolean |
true |
Open the popup when the input receives focus. |
autoHighlight |
boolean |
false |
Auto-highlight the first visible item when filtering. |
filter |
(inputValue: string, itemValue: string, itemLabel: string) => boolean |
— | Custom filter function. Return true to show the item. |
onValueChange |
(value: string | null) => void |
— | Called when the selected value changes. |
onOpenChange |
(open: boolean) => void |
— | Called when the popup opens or closes. |
onInputValueChange |
(inputValue: string) => void |
— | Called when the user types in the input (not on programmatic syncs). |
side |
"top" | "bottom" |
"bottom" |
Preferred side of the popup relative to the trigger. |
align |
"start" | "center" | "end" |
"start" |
Alignment of the popup relative to the trigger. |
sideOffset |
number |
4 |
Distance from the trigger in pixels. |
alignOffset |
number |
0 |
Offset along the alignment axis in pixels. |
avoidCollisions |
boolean |
true |
Flip the popup to stay inside the viewport when it would overflow. |
collisionPadding |
number |
8 |
Viewport edge padding used for collision detection. |
itemToStringValue |
(item: HTMLElement | null, value: string | null) => string |
— | Custom text resolver for the selected-value display. |
Controller
| Member | Type | Description |
|---|---|---|
value |
string | null (getter) |
Current selected value. |
inputValue |
string (getter) |
Current input text. |
isOpen |
boolean (getter) |
Current open state. |
select(value) |
(value: string) => void |
Select a value programmatically. |
clear() |
() => void |
Clear the selected value. |
open() |
() => void |
Open the popup. |
close() |
() => void |
Close the popup. |
setItemToStringValue |
(fn: (item: HTMLElement | null, value: string | null) => string | null) => void |
Set or clear a runtime selected-value text resolver. |
destroy() |
() => void |
Remove all listeners, observers, and clean up. |
Controller
The controller is returned by Combobox.createCombobox() and provides imperative control over the combobox’s state.
const controller = Combobox.createCombobox(root, {
placeholder: "Choose an option...",
});
// Select a value programmatically
controller.select("apple");
// Clear the selection
controller.clear();
// Open and close the popup
controller.open();
controller.close();
// Read current state
console.log(controller.value); // "apple" | null
console.log(controller.inputValue); // "ap"
console.log(controller.isOpen); // true | false
// Set a custom value display resolver
controller.setItemToStringValue((item, value) => {
return item?.getAttribute("data-label") ?? value ?? "";
});
// Clean up
controller.destroy();