Skip to content

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

SlotRequiredDescription
comboboxYesRoot container.
combobox-inputYesText input for typing and filtering.
combobox-triggerNoButton that opens and closes the popup.
combobox-contentYesPopup panel that contains the list and optionally an input.
combobox-listYesScrollable container for items, groups, separators, and empty slot.
combobox-itemYesSelectable option. Requires data-value.
combobox-groupNoWraps a set of related items with a label.
combobox-labelNoVisible heading inside a group.
combobox-emptyNoMessage shown when no items match the filter.
combobox-separatorNoVisual divider between items or groups.
combobox-clearNoButton that clears the current selection.
combobox-valueNoDisplays the selected value inside a button trigger (popup-input mode).

Generated Attributes

AttributeElement(s)Description
data-stateRoot"open" or "closed".
data-highlightedcombobox-itemPresent on the currently highlighted item.
data-selectedcombobox-itemPresent on the currently selected item.
data-disabledcombobox-itemPresent when the item is disabled.
aria-expandedcombobox-input, combobox-trigger"true" when open, "false" when closed.
aria-activedescendantRootPoints to the highlighted item id when keyboard-navigating.
rolecombobox-inputSet to "combobox".
rolecombobox-listSet to "listbox".
rolecombobox-itemSet to "option".
rolecombobox-groupSet to "group".
hiddencombobox-contentToggled based on open state.

Events

Outbound

EventDetailDescription
combobox:change{ value: string | null, label: string | null, open?: boolean }Fired on the root when the selected value changes.

Inbound

EventDetailDescription
combobox:select{ value: string }Dispatch on the root to programmatically select a value.

API Reference

Options

OptionTypeDefaultDescription
defaultValuestringInitial selected value.
defaultOpenbooleanfalseInitial open state.
placeholderstringPlaceholder text for the input.
disabledbooleanfalseDisable interaction.
requiredbooleanfalseMark the field as required for form validation.
namestringForm field name. Creates a hidden input for native form submission.
openOnFocusbooleantrueOpen the popup when the input receives focus.
autoHighlightbooleanfalseAuto-highlight the first visible item when filtering.
filter(inputValue: string, itemValue: string, itemLabel: string) => booleanCustom filter function. Return true to show the item.
onValueChange(value: string | null) => voidCalled when the selected value changes.
onOpenChange(open: boolean) => voidCalled when the popup opens or closes.
onInputValueChange(inputValue: string) => voidCalled 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.
sideOffsetnumber4Distance from the trigger in pixels.
alignOffsetnumber0Offset along the alignment axis in pixels.
avoidCollisionsbooleantrueFlip the popup to stay inside the viewport when it would overflow.
collisionPaddingnumber8Viewport edge padding used for collision detection.
itemToStringValue(item: HTMLElement | null, value: string | null) => stringCustom text resolver for the selected-value display.

Controller

MemberTypeDescription
valuestring | null (getter)Current selected value.
inputValuestring (getter)Current input text.
isOpenboolean (getter)Current open state.
select(value)(value: string) => voidSelect a value programmatically.
clear()() => voidClear the selected value.
open()() => voidOpen the popup.
close()() => voidClose the popup.
setItemToStringValue(fn: (item: HTMLElement | null, value: string | null) => string | null) => voidSet or clear a runtime selected-value text resolver.
destroy()() => voidRemove 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();