---
title: Combobox
description: An input with a filterable list of suggestions and keyboard selection.
---

<PackageInfo name="combobox" />

**bun**

```bash
bun add @data-slot/combobox
```

**npm**

```bash
npm install @data-slot/combobox
```

**pnpm**

```bash
pnpm add @data-slot/combobox
```

## Anatomy

The input and content are required; the rest is optional. `combobox-trigger` toggles the popup, `combobox-empty` appears when nothing matches, and `combobox-item-indicator` is hidden automatically on unselected items.

```html
<div data-slot="combobox" data-placeholder="Search fruits...">
  <input data-slot="combobox-input" />
  <button data-slot="combobox-trigger">▼</button>
  <div data-slot="combobox-content" hidden>
    <div data-slot="combobox-list">
      <div data-slot="combobox-empty">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
          <span data-slot="combobox-item-indicator">✓</span>
        </div>
        <div data-slot="combobox-item" data-value="banana">
          Banana
          <span data-slot="combobox-item-indicator">✓</span>
        </div>
      </div>
      <div data-slot="combobox-separator"></div>
      <div data-slot="combobox-item" data-value="other">Other</div>
    </div>
  </div>
</div>
```

## Examples

### Filter as you type

<Example name="combobox" />

### Choose an initial value

`data-default-value="banana"` preselects the matching option on load.

<Example name="combobox" variant="extra" />

## API reference

### Initialization

#### `create(scope?)`

Find and bind uninitialized `[data-slot="combobox"]` descendants of `scope` (defaults to `document`). Returns `ComboboxController[]` for newly bound roots. To initialize the scope element itself, use `createCombobox`.

```typescript
import { create } from "@data-slot/combobox";

const controllers = create();
```

#### `createCombobox(root, options?)`

Create a `ComboboxController` for one root element. JavaScript options take precedence over the corresponding data attributes. Calling this again for a bound root returns its existing controller; destroy it before rebinding with new options.

```typescript
import { createCombobox } from "@data-slot/combobox";

const controller = createCombobox(element, {});
```

### Slots

#### Runtime Slots

- `combobox` - Root container.
- `combobox-input` - Required text input for filtering and keyboard navigation.
- `combobox-trigger` - Optional button that toggles the popup.
- `combobox-clear` - Optional button that clears the current value and focuses input.
- `combobox-value` - Optional selected-value text target (typically inside `combobox-trigger`).
- `combobox-content` - Required popup container for the search results.
- `combobox-list` - Optional scrollable listbox wrapper; the content acts as the listbox when omitted.
- `combobox-item` - Individual selectable option.
- `combobox-item-indicator` - Optional selected-state indicator inside `combobox-item`; hidden automatically for unselected items.
- `combobox-group` - Groups related items.
- `combobox-label` - Group label (inside a `combobox-group`).
- `combobox-separator` - Divider between items or groups; its visibility is updated as results are filtered.
- `combobox-empty` - Message shown when no items match filter.
- `combobox-positioner` - Optional authored positioning wrapper (reused instead of generated wrapper).
- `combobox-portal` - Optional authored portal wrapper that can contain `combobox-positioner`.

#### Composed Portal Markup (Optional)

```html
<div data-slot="combobox">
  <input data-slot="combobox-input" />
  <button data-slot="combobox-trigger">▼</button>
  <div data-slot="combobox-portal">
    <div data-slot="combobox-positioner">
      <div data-slot="combobox-content" hidden>...</div>
    </div>
  </div>
</div>
```

#### Popup-Input Composition (Optional)

Use this when you want a trigger with committed value text and a separate search input inside the popup.

```html
<div data-slot="combobox">
  <button data-slot="combobox-trigger">
    <span data-slot="combobox-value">Select country...</span>
  </button>
  <div data-slot="combobox-content" hidden>
    <input data-slot="combobox-input" placeholder="Search countries..." />
    <div data-slot="combobox-list">...</div>
  </div>
</div>
```

In popup-input mode (`combobox-input` inside `combobox-content`):
- Selection text is synced to `combobox-value`.
- Search input is cleared each time the popup opens.
- Closing keeps the popup input empty.

#### Clear Button (Optional)

```html
<div data-slot="combobox">
  <input data-slot="combobox-input" />
  <button data-slot="combobox-clear">Clear</button>
  <div data-slot="combobox-content" hidden>...</div>
</div>
```

When `combobox-clear` is clicked:
- Current value is cleared.
- Input is focused.
- If popup is closed, it remains closed.
- If popup is open, it remains open.
- By default it is out of keyboard tab order (`tabindex="-1"` behavior). Set `tabindex="0"` to make it tabbable.

#### Keyboard Tab Stops

- `combobox-input` is the primary keyboard tab stop.
- `combobox-trigger` and `combobox-clear` are out of tab order by default.
- To opt into keyboard tabbing for either control, author `tabindex="0"` on that element.

#### Native Label Support

Use a standard HTML `<label for="...">` element to label the combobox. The `for` attribute should match the `id` on the input. Clicking the label focuses the input, and `aria-labelledby` is set automatically.

```html
<label for="fruit-input">Choose a fruit</label>
<div data-slot="combobox">
  <input data-slot="combobox-input" id="fruit-input" />
  <div data-slot="combobox-content" hidden>
    <div data-slot="combobox-list">
      <div data-slot="combobox-item" data-value="apple">Apple</div>
    </div>
  </div>
</div>
```

### Options

Options can be passed via JavaScript or data attributes (JS takes precedence).

| Option | Data Attribute | Type | Default | Description |
|--------|---------------|------|---------|-------------|
| `defaultValue` | `data-default-value` | `string` | `null` | Initial selected value |
| `defaultOpen` | `data-default-open` | `boolean` | `false` | Initial popup open state |
| `placeholder` | `data-placeholder` | `string` | `""` | Input placeholder text |
| `disabled` | `data-disabled` | `boolean` | `false` | Disable interaction |
| `required` | `data-required` | `boolean` | `false` | Form validation required |
| `name` | `data-name` | `string` | - | Form field name (creates hidden input) |
| `openOnFocus` | `data-open-on-focus` | `boolean` | `true` | Open popup when input is focused |
| `autoHighlight` | `data-auto-highlight` | `boolean` | `false` | Auto-highlight first visible item after non-whitespace query input |
| `filter` | - | `(inputValue: string, itemValue: string, itemLabel: string) => boolean` | case-insensitive label substring match | Return `true` to show an item |
| `itemToStringValue` | - | `(item: HTMLElement \| null, value: string \| null) => string` | item label | Custom text resolver for committed selected-value text (input in inline mode, `combobox-value` in popup-input mode) |
| `side` | `data-side` | `"top" \| "bottom"` | `"bottom"` | Popup placement |
| `align` | `data-align` | `"start" \| "center" \| "end"` | `"start"` | Popup alignment |
| `sideOffset` | `data-side-offset` | `number` | `4` | Distance from input (px) |
| `alignOffset` | `data-align-offset` | `number` | `0` | Offset from alignment edge (px) |
| `avoidCollisions` | `data-avoid-collisions` | `boolean` | `true` | Adjust to stay in viewport |
| `collisionPadding` | `data-collision-padding` | `number` | `8` | Viewport edge padding (px) |

Placement attributes (`data-side`, `data-align`, `data-side-offset`, `data-align-offset`, `data-avoid-collisions`, `data-collision-padding`) resolve in this order:

1. JavaScript option
2. `combobox-content`
3. `combobox-positioner`
4. `combobox` root (fallback)

The positioned element (`combobox-positioner`, or `combobox-content` when no positioner is used) receives these CSS variables for styling:

- `--transform-origin`
- `--available-width`
- `--available-height`
- `--anchor-width`
- `--anchor-height`

When a separate `combobox-positioner` is present, the same values are also mirrored onto `combobox-content` so copied style packs can branch from either element.

#### Mobile Behavior

- On touch/coarse-pointer environments, outside `pointerdown` (for example during scroll gestures) does not dismiss the popup.
- Outside tap/click still dismisses the popup.
- While open, popup position tracks scroll so the popup stays anchored.
- On touch/coarse-pointer environments, combobox always positions on the `bottom` side.
- On touch/coarse-pointer environments, collision side-flipping is disabled to avoid jumpy repositioning.

#### Callbacks

| Callback | Type | Description |
|----------|------|-------------|
| `onValueChange` | `(value: string \| null) => void` | Called when selection changes |
| `onOpenChange` | `(open: boolean) => void` | Called when popup opens/closes |
| `onInputValueChange` | `(inputValue: string) => void` | Called when user types in the input |

### Controller

```typescript
interface ComboboxController {
  readonly value: string | null;      // Current selected value
  readonly inputValue: string;        // Current input text
  readonly isOpen: boolean;           // Current open state
  select(value: string): void;        // Select a value
  clear(): void;                      // Clear selection
  open(): void;                       // Open the popup
  close(): void;                      // Close the popup
  setItemToStringValue(itemToStringValue: ((item: HTMLElement | null, value: string | null) => string) | null): void;
  destroy(): void;                    // Cleanup
}
```

#### Controller Destruction

`destroy()` permanently disposes the controller and hides any open surface without
emitting an additional change event. Repeated destruction is safe; methods on the
old controller become no-ops. Create a new controller on the same root to rebind it.

### Events

#### Outbound Events

```javascript
root.addEventListener('combobox:change', (e) => {
  console.log('Value changed:', e.detail.value);
});

root.addEventListener('combobox:open-change', (e) => {
  console.log('Open state:', e.detail.open);
});

root.addEventListener('combobox:input-change', (e) => {
  console.log('Input changed:', e.detail.inputValue);
});
```

#### Inbound Events

`combobox:set` accepts a partial update with these fields, applied in table order:

| Field | Type | Description |
| --- | --- | --- |
| `value` | `string \| null` | Select a value, or clear with `null` |
| `open` | `boolean` | Open or close the popup; opening is blocked when disabled |
| `inputValue` | `string` | Replace the input text without filtering or firing the input-change callback |
| `itemToStringValue` | `ComboboxItemToStringValue \| null` | Replace or clear the selected-value formatter and resync the displayed value |

Programmatic selection works while disabled. In inline-input mode, updating the formatter resynchronizes the input, so it can overwrite `inputValue` supplied in the same event.

```javascript
// Set value
root.dispatchEvent(new CustomEvent('combobox:set', {
  detail: { value: 'apple' }
}));

// Set open state
root.dispatchEvent(new CustomEvent('combobox:set', {
  detail: { open: true }
}));

// Set runtime selected-value text formatter
root.dispatchEvent(new CustomEvent('combobox:set', {
  detail: { itemToStringValue: (item, value) => value ? value.toUpperCase() : '' }
}));
```

### Keyboard Navigation

| Key | Action |
|-----|--------|
| `ArrowDown` | Open popup (when closed); move to next visible item |
| `ArrowUp` | Open popup (when closed); move to previous visible item |
| `Home` | Move to first visible item |
| `End` | Move to last visible item |
| `Enter` | Select highlighted item |
| `Escape` | Close popup, restore input to committed value |
| `Tab` | Close popup, restore input, allow normal tab flow |

### Accessibility

- Input: `role="combobox"`, `aria-expanded`, `aria-controls`, `aria-activedescendant`, `aria-autocomplete="list"`
- List: `role="listbox"`, `aria-labelledby`
- Item: `role="option"`, `aria-selected`, `aria-disabled`
- Group: `role="group"`, `aria-labelledby`
- Disabled items are skipped during keyboard navigation

### Form Integration

When `name` is provided, a hidden input is automatically created for form submission:

```html
<form>
  <div data-slot="combobox" data-name="fruit">
    <input data-slot="combobox-input" />
    <div data-slot="combobox-content" hidden>
      <div data-slot="combobox-list">
        <div data-slot="combobox-item" data-value="apple">Apple</div>
      </div>
    </div>
  </div>
  <button type="submit">Submit</button>
</form>
```

An authored `name` on the combobox input is also supported. The generated input
submits the selected value; the visible input keeps the search or display text.
Destroying the controller removes the generated input and restores the authored name.

**Behavior change:** earlier releases left an authored `name` on the visible input,
so the form received the displayed or typed text. It now receives the selected value
(or an empty string when nothing is selected). To submit free text, keep `name` off the
combobox input and mirror `combobox:input-change` into a separate field.
A disabled combobox does not submit its value.

Resetting the form restores `defaultValue` and the displayed selection without
emitting a value-change event. An open popup also resets its search and highlight.
Synchronization happens on the next event-loop task, after the browser resets
native controls. Calling `preventDefault()` on the reset event preserves the current state.
