---
title: Combobox
description: A searchable select component that filters and selects from a list of options with keyboard navigation and floating-positioned content.
order: 5
tags: [primitives]
---

# 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.

```ts
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

```ts
import { Combobox } from "@areia/slots";
```

## Usage

```ts
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.

```html
<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.

```html
<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.

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