Combobox
Filterable autocomplete input. Full keyboard navigation, ARIA listbox semantics, and a render-prop items API. Controlled and uncontrolled.
Features
Example
Anatomy
import { Combobox } from '@wire-ui/react'
const options = [
{ value: 'react', label: 'React' },
{ value: 'vue', label: 'Vue' },
{ value: 'solid', label: 'Solid' },
]
<Combobox.Root options={options}>
<Combobox.Input placeholder="Search…" />
<Combobox.Trigger>▾</Combobox.Trigger>
<Combobox.Content>
<Combobox.Items>
{({ option }) => <div>{option.label}</div>}
</Combobox.Items>
<Combobox.Empty>No results</Combobox.Empty>
</Combobox.Content>
</Combobox.Root>Styling
Each option row gets data-highlighted, data-selected, and data-disabled. Because the row uses class="group" internally, you can target these from anywhere inside the row with the group-data-* Tailwind variant.
<Combobox.Items>
{({ option }) => (
<div className="
px-3 py-2
group-data-[highlighted]:bg-[#f5f5f5]
group-data-[selected]:font-semibold
group-data-[disabled]:opacity-40
">
{option.label}
</div>
)}
</Combobox.Items>Using data attributes
Root sets data-state and data-disabled. Trigger mirrors data-state and exposes Button-style interactive attributes. Each option row sets data-highlighted, data-selected, data-disabled.
[data-state="open"] .trigger { transform: rotate(180deg); }
[data-highlighted] { background: #f5f5f5; }
[data-selected] { font-weight: 600; }
[data-disabled] { opacity: 0.4; cursor: not-allowed; }Root props
| Prop | Type | Default | Description |
|---|---|---|---|
options | ComboboxOption[] | required | Available options |
value | string | null | — | Controlled selected value |
defaultValue | string | null | null | Initial selected value (uncontrolled) |
onChange | (value, option) => void | — | Called when the selection changes |
inputValue | string | — | Controlled input text |
defaultInputValue | string | — | Initial input text (uncontrolled) |
onInputChange | (value: string) => void | — | Called when the user types |
filter | (option, input) => boolean | case-insensitive label match | Custom filter |
disabled | boolean | false | Disables the combobox |
open | boolean | — | Controlled open state |
defaultOpen | boolean | false | Initial open state |
onOpenChange | (open: boolean) => void | — | Called when open state changes |
ComboboxOption
| Field | Type | Description |
|---|---|---|
value | string | Stable identifier |
label | string | Human-readable label shown in the input when selected |
subtitle | string | Optional subtitle available to the render prop |
disabled | boolean | Disable selection of this option |
Items render-prop
Combobox.Items accepts a function child that receives { option, highlighted, selected } for each filtered row.
<Combobox.Items>
{({ option, highlighted, selected }) => (
<div>{option.label}</div>
)}
</Combobox.Items>Empty
Combobox.Empty only renders when the filtered list is empty.
Data attributes
| Attribute | Element | Values |
|---|---|---|
data-state | Root, Trigger, Content | "open" / "closed" |
data-disabled | Root, Trigger, Item | Present when disabled |
data-highlighted | Item | Present on the currently highlighted row |
data-selected | Item | Present on the selected row |
data-hover / data-active / data-focus-visible | Trigger | Standard interactive states |
Input also sets role="combobox", aria-expanded, aria-controls, and aria-activedescendant. Content sets role="listbox". Each row sets role="option" with aria-selected and aria-disabled.
Accessibility
- Implements the WAI-ARIA combobox pattern with
aria-autocomplete="list". - Highlighted option is announced via
aria-activedescendant— focus stays in the input. - Disabled options are skipped during keyboard navigation.
Keyboard Interactions
| Key | Description |
|---|---|
| ArrowDown | Opens the list, or moves highlight down. |
| ArrowUp | Opens the list, or moves highlight up. |
| Home | Highlights the first option. |
| End | Highlights the last option. |
| Enter | Selects the highlighted option. |
| Escape | Closes the list. |