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 { Combobox } from "@areia/slots";
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();
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>
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>
| 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). |
| 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. |
| Event | Detail | Description |
|---|
combobox:change | { value: string | null, label: string | null, open?: boolean } | Fired on the root when the selected value changes. |
| Event | Detail | Description |
|---|
combobox:select | { value: string } | Dispatch on the root to programmatically select a value. |
| 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. |
| 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. |
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();