Skip to content

Combobox

A searchable select component that filters and selects from a list of options with keyboard navigation and floating-positioned content.

Updated View as Markdown

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>

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();
Navigation

Type to search…

↑↓ navigate↵ selectEsc close