Skip to content
Areia
Esc
↑↓navigate↵open⌘Jpreview
On this page

Combobox

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

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

Was this page helpful?