# Combobox

Combobox is a neobrutalist React component from Cruise UI. A filterable select: type to filter, grouped options, async loading and multi-select chips.

Install (with the @cruiseui registry in components.json, see Installation):

```bash
npx shadcn@latest add @cruiseui/combobox
```

Import: `import { CruiseCombobox } from "@/components/cruise/combobox";`

## When to use

Long lists, or when people know what they're looking for (their state, their bank).

## When not to

Under ~8 options (use a native Select).

## Example (React)

```tsx
"use client";

import { CruiseCombobox, type CruiseComboGroup } from "@/components/cruise/combobox";

const opt = (label: string) => ({ value: label.toLowerCase().replace(/\s+/g, "-"), label });

const states: CruiseComboGroup[] = [
  { label: "South South", items: ["Akwa Ibom", "Bayelsa", "Cross River", "Delta", "Edo", "Rivers"].map(opt) },
  { label: "South West", items: ["Ekiti", "Lagos", "Ogun", "Ondo", "Osun", "Oyo"].map(opt) },
];

const cities = ["Lagos", "Ibadan", "Abuja", "Port Harcourt", "Kano", "Enugu"].map(opt);

export default function ComboboxExample() {
  return (
    <div className="flex max-w-md flex-col gap-6">
      <CruiseCombobox label="State" items={states} placeholder="Search states" hint="Try typing “lag” or “river”." />
      <CruiseCombobox multiple label="Pick your cities" items={cities} defaultValue={[cities[0], cities[1]]} />
    </div>
  );
}
```

## HTML

Needs theme.css (https://cruiseui.dev/theme.css).

```html
<div class="field combobox" data-combobox>
  <label class="field-label" for="state-input">State</label>
  <div class="combobox-sheet">
    <div class="combobox-field">
      <svg class="combobox-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><circle cx="11" cy="11" r="7"/><path d="m20 20-4-4"/></svg>
      <input id="state-input" class="combobox-input" role="combobox" aria-expanded="false" aria-controls="state-list" aria-autocomplete="list" autocomplete="off" placeholder="Search states">
      <button type="button" class="combobox-btn" data-clear aria-label="Clear" hidden>
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12"/></svg></button>
      <button type="button" class="combobox-btn" data-toggle aria-label="Show state options" tabindex="-1">
        <svg class="menu-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></button>
    </div>
    <div class="menu combobox-popup" hidden>
      <div class="combobox-status" role="status"></div>
      <div role="listbox" id="state-list" aria-label="States">
        <div role="group" aria-labelledby="zone-ss">
          <div class="menu-group-label" id="zone-ss">South South</div>
          <div class="menu-item" role="option" id="state-delta" aria-selected="false"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span class="combobox-label">Delta</span></div>
          <div class="menu-item" role="option" id="state-rivers" aria-selected="false"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span class="combobox-label">Rivers</span></div>
        </div>
        <div role="group" aria-labelledby="zone-sw">
          <div class="menu-group-label" id="zone-sw">South West</div>
          <div class="menu-item" role="option" id="state-lagos" aria-selected="false"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span class="combobox-label">Lagos</span></div>
          <div class="menu-item" role="option" id="state-oyo" aria-selected="false"><svg class="menu-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg><span class="combobox-label">Oyo</span></div>
        </div>
      </div>
    </div>
  </div>
  <div class="drawer-backdrop" hidden></div>
</div>
<!-- Multi-select: add data-multiple to .combobox; chips are added before the input.
     Error: data-invalid on .combobox-field, aria-invalid + aria-describedby on the input, then
     <p class="field-error" id="...">Choose a delivery state.</p>. Loading: put
     <span class="spinner" aria-hidden="true"></span> Searching… in .combobox-status. -->
```

## Props: CruiseCombobox

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label?` | `string` | — | Visible label. Leave out inside a CruiseFormField (it brings the label). |
| `items` | `CruiseComboOption[] \| CruiseComboGroup[]` | — | Options, or groups of options with a heading each. |
| `placeholder?` | `string` | — | — |
| `hint?` | `string` | — | — |
| `error?` | `string` | — | Error message: --danger border and text under the field. |
| `disabled?` | `boolean` | — | — |
| `searchIcon?` | `boolean` | — | Search icon at the start of the field. |
| `loading?` | `boolean` | — | Async lists: shows the spinner + "Searching…". |
| `onInputChange?` | `((query: string) => void)` | — | Called as people type (e.g. to fetch matches). |
| `remote?` | `boolean` | — | The list is already filtered (e.g. by a server), so don't filter it again. |
| `popupClassName?` | `string` | — | Extra classes on the list / sheet, e.g. "nepa-mode" (they render in <body>). |
| `className?` | `string` | — | — |
| `multiple?` | `boolean` | — | — |
| `value?` | `CruiseComboOption \| null \| CruiseComboOption[]` | — | — |
| `defaultValue?` | `CruiseComboOption \| null \| CruiseComboOption[]` | — | — |
| `onValueChange?` | `((value: CruiseComboOption \| null) => void) \| ((value: CruiseComboOption[]) => void)` | — | — |

## Accessibility

- ARIA combobox pattern: aria-expanded, aria-controls, aria-activedescendant; listbox / option / aria-selected.
- Works with the keyboard only; "No matches" and "Searching…" are announced.
- Below 640px it opens as a bottom sheet with the search focused.
