# Combobox

A combobox lets people search a long list and choose one value from it.

A combobox shows the current value in a field and opens a list with a search box, so people can type to narrow a long list before they choose. For a short list that people can scan without searching, use a [Select](/docs/select). For a list of actions rather than values, use a [Dropdown Menu](/docs/dropdown-menu).


## Usage

```tsx
import { Combobox } from '@kushagradhawan/kookie-ui';

const countries: Record<string, string> = { in: 'India', jp: 'Japan', gb: 'United Kingdom' };
const countryName = (code: string | null) => (code ? countries[code] : undefined);

export function CountryFilter({ value, onChange }: { value: string | null; onChange: (value: string | null) => void }) {
  return (
    <Combobox.Root value={value} onValueChange={onChange} displayValue={countryName} placeholder="Any country">
      <Combobox.Trigger aria-label="Country" />
      <Combobox.Content>
        <Combobox.Input placeholder="Search countries" />
        <Combobox.List>
          <Combobox.Empty>No countries found</Combobox.Empty>
          {Object.entries(countries).map(([code, name]) => (
            <Combobox.Item key={code} value={code}>
              {name}
            </Combobox.Item>
          ))}
        </Combobox.List>
      </Combobox.Content>
    </Combobox.Root>
  );
}
```

## Anatomy

```tsx
<Combobox.Root>
  <Combobox.Trigger />
  <Combobox.Content>
    <Combobox.Input />
    <Combobox.List>
      <Combobox.Empty />
      <Combobox.Group>
        <Combobox.Label />
        <Combobox.Item />
      </Combobox.Group>
      <Combobox.Separator />
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
```

- **Root:** Holds the value, the search text and the open state, and sets the size.
- **Trigger:** The field that shows the chosen value and opens the list.
- **Value:** The chosen value as text, for a trigger with a custom layout.
- **Content:** The panel that opens below the trigger, rendered in a portal.
- **Input:** The search box at the top of the panel.
- **List:** The scrolling list of options.
- **Empty:** Shows when the search matches no option.
- **Item:** One option. The chosen item shows a check mark.
- **Group, Label and Separator:** Sections of the list, a heading for a section, and a line between sections.

## Best practices

**Use a combobox when people know what they're looking for.** Countries, time zones, people and repositories are long lists with known names, so typing a few letters is faster than scrolling. For fewer than about ten options, a [Select](/docs/select) is quicker to read.

**Give the field a visible label.** The trigger has the `combobox` role, which doesn't take its name from its content. Point a `label` at the trigger's `id`, or set `aria-label`. The same name labels the panel, the search box and the list.

**Always add an Empty message.** Without it, a search that matches nothing shows a blank panel, and people can't tell whether the list is still loading.

**Show a label, not the stored value.** Set `displayValue` on `Root` so the trigger shows "United Kingdom" rather than "gb". Without it, the trigger shows the raw value.

**Search the words people use.** Search matches the text of each item. Add `keywords` for other names, such as "UK" and "Britain" for the United Kingdom.

## Variant

`variant` on `Trigger` sets the look of the field. `raised` is the default and suits most forms. Use `surface` or `outline` to match a [TextField](/docs/text-field) with the same variant, `soft` on a tinted background, and `ghost` in a toolbar or a table cell. The search box uses its own `variant` on `Input`, `soft` by default.

## Size

`size` on `Root` picks one of four heights for the trigger: 28, 32, 40 and 48 pixels. The panel, the search box and the items use the same size unless you set `size` on `Content`. Size 2 is the default. `size` accepts a responsive object, such as `{ initial: '3', md: '2' }`.

## Examples

### Search on the server

Set `shouldFilter={false}` when your API does the search. Control `searchValue` to send each query, and render only the items the server returns.

```tsx
<Combobox.Root
  shouldFilter={false}
  searchValue={query}
  onSearchValueChange={setQuery}
>
  <Combobox.Trigger aria-label="Assignee" loading={isFetching} />
  <Combobox.Content>
    <Combobox.Input placeholder="Search people" />
    <Combobox.List>
      <Combobox.Empty>No one matches "{query}"</Combobox.Empty>
      {results.map((person) => (
        <Combobox.Item key={person.id} value={person.id}>
          {person.name}
        </Combobox.Item>
      ))}
    </Combobox.List>
  </Combobox.Content>
</Combobox.Root>
```

While `loading` or `readOnly` is set, the trigger stays focusable but doesn't open.

### Grouped options

Group items under a `Label` when the list has natural sections, such as recent and all projects. A group whose items all fail the search hides itself.

```tsx
<Combobox.List>
  <Combobox.Group>
    <Combobox.Label>Recent</Combobox.Label>
    <Combobox.Item value="atlas">Atlas</Combobox.Item>
  </Combobox.Group>
  <Combobox.Separator />
  <Combobox.Group>
    <Combobox.Label>All projects</Combobox.Label>
    <Combobox.Item value="beacon">Beacon</Combobox.Item>
    <Combobox.Item value="compass">Compass</Combobox.Item>
  </Combobox.Group>
</Combobox.List>
```

### Rich items

An item can hold an avatar or a second line of text. Search reads the text inside the children. When the children are components whose text isn't in the JSX, set `label` to the text to search.

```tsx
<Combobox.Item value={user.id} label={user.name}>
  <UserRow user={user} />
</Combobox.Item>
```

### Invalid value

Set `error` on `Trigger` when a form check fails. The trigger shows a red border and sets `aria-invalid`. Put the message in text next to the field, because the colour alone doesn't say what is wrong.

## Props

### Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string \| null` | | The chosen value, when you control it |
| `defaultValue` | `string \| null` | `null` | The value at first render |
| `onValueChange` | `(value: string \| null) => void` | | Called when someone chooses an item |
| `displayValue` | `string \| (value) => string \| undefined` | | The text the trigger shows for the value. Pass a stable function |
| `placeholder` | `string` | `'Select an option'` | Shows in the trigger when no value is chosen |
| `searchPlaceholder` | `string` | `'Search...'` | Shows in the empty search box |
| `searchValue` | `string` | | The search text, when you control it |
| `defaultSearchValue` | `string` | `''` | The search text at first render |
| `onSearchValueChange` | `(value: string) => void` | | Called when the search text changes |
| `shouldFilter` | `boolean` | `true` | Filters and sorts items by the search. Set `false` to filter yourself |
| `filter` | `(value, search, keywords) => number` | | Your own match score, from 0 (hidden) to 1 |
| `resetSearchOnSelect` | `boolean` | `true` | Clears the search when an item is chosen |
| `resetSearchOnClose` | `boolean` | `true` | Clears the search when the panel closes without a choice |
| `loop` | `boolean` | `true` | Wraps the arrow keys from the last item to the first |
| `open` | `boolean` | | Whether the panel is open, when you control it |
| `defaultOpen` | `boolean` | `false` | Whether the panel is open at first render |
| `onOpenChange` | `(open: boolean) => void` | | Called when the panel opens or closes |
| `size` | `'1' \| '2' \| '3' \| '4'`, responsive | `'2'` | The height step |
| `color` | `AccentColor` | Theme accent | The colour of the trigger and the panel |
| `disabled` | `boolean` | `false` | Disables the trigger and every item |
| `modal` | `boolean` | `false` | Blocks the rest of the page while the panel is open |

### Trigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'raised' \| 'surface' \| 'soft' \| 'outline' \| 'ghost'` | `'raised'` | The look of the field |
| `placeholder` | `string` | Root's `placeholder` | Shows when no value is chosen |
| `color` | `AccentColor` | Root's `color` | The colour |
| `radius` | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | Theme radius | The corner |
| `width` | `string`, responsive | | The CSS width |
| `error` | `boolean` | `false` | Shows the invalid state and sets `aria-invalid` |
| `loading` | `boolean` | `false` | Shows a spinner, sets `aria-busy` and doesn't open |
| `readOnly` | `boolean` | `false` | Sets `aria-readonly` and doesn't open |
| `disabled` | `boolean` | Root's `disabled` | Disables the trigger |

Trigger also accepts every attribute of the HTML `button` element. A single child element replaces the default layout of the value and the chevron. While `loading`, the spinner still shows after it.

### Value

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | Root's `placeholder` | Shows when no value is chosen |

### Content

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `size` | `'1' \| '2' \| '3' \| '4'`, responsive | Root's `size` | The height step of the panel |
| `color` | `AccentColor` | Root's `color` | The colour of the highlight |
| `width`, `minWidth` | `string`, responsive | | The CSS width and minimum width |
| `maxWidth` | `string`, responsive | `'480px'` | The CSS maximum width |
| `height`, `minHeight`, `maxHeight` | `string`, responsive | | The CSS height limits |
| `side` | `'top' \| 'right' \| 'bottom' \| 'left'` | `'bottom'` | Where the panel opens |
| `align` | `'start' \| 'center' \| 'end'` | `'start'` | How the panel lines up with the trigger |
| `sideOffset` | `number` | `4` | The gap to the trigger in pixels |
| `container` | `HTMLElement` | `document.body` | The element the portal renders into |

### Input

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'raised' \| 'surface' \| 'soft' \| 'ghost' \| 'outline'` | `'soft'` | The look of the search box |
| `placeholder` | `string` | Root's `searchPlaceholder` | Shows in the empty search box |
| `startAdornment`, `endAdornment` | `ReactNode` | | Content before or after the text, such as an icon |

Input also accepts the attributes of the HTML `input` element.

### Item

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `string` | The text of the children | The value this item sets |
| `label` | `string` | The text of the children | The text that search matches |
| `keywords` | `string[]` | | More words that search matches |
| `disabled` | `boolean` | Root's `disabled` | Stops the item from being chosen |
| `onSelect` | `(value: string) => void` | | Called when this item is chosen |

`Group` accepts `heading` and `forceMount`. `Separator` accepts `alwaysRender`, which keeps it visible during a search. `List` accepts `label`, which defaults to the trigger's name. Each part also accepts the attributes of the HTML `div` element.

## Accessibility

| Key | Action |
| --- | --- |
| Enter, Space, Arrow Down or Arrow Up | Opens the panel from the trigger |
| A letter key | Types into the search box and filters the list |
| Arrow Down or Arrow Up | Moves the highlight through the items |
| Home or End | Moves the highlight to the first or last item |
| Enter | Chooses the highlighted item and closes the panel |
| Escape | Closes the panel without changing the value |

- **Name:** The trigger needs a name from a `label`, `aria-label` or `aria-labelledby`. In development, a trigger without one logs a warning. The panel, the search box and the list take the same name.
- **Announcements:** The trigger announces its name, the value and whether the panel is expanded. After typing pauses, a live region announces the number of results, or "No results".
- **Selection:** The highlighted item sets `aria-selected`, and the chosen item sets `aria-checked`. Group labels name their groups.
- **Focus:** Focus moves to the search box when the panel opens, and the highlight starts on the chosen item. Without an `Input`, focus moves to the list so the keys still work. Focus returns to the trigger when the panel closes.
