# Combobox

> A text field that filters a list as you type, with groups, a check on the picked item, and chips for picking several.

- Name: `combobox`
- Status: beta
- Categories: forms
- File: the install writes `combobox.tsx` to your `ui` alias, `@/components/ui` by default.
- npm packages: `@base-ui/react`. The install adds them.
- Other registry items: `@arbokui/chip-glide`, `@arbokui/glide-highlight`, `@arbokui/input`, `@arbokui/tooltip`. The install adds them.
- Gallery: https://arbokui.com/components/combobox
- Registry item: https://arbokui.com/r/combobox.json

## Install

1. Add the registry to `components.json`. Do this first, once:

   ```bash
   npx shadcn@latest registry add "@arbokui=https://arbokui.com/r/{name}.json"
   ```

   The command writes this entry. You can also add it by hand:

   ```json
   {
     "registries": {
       "@arbokui": "https://arbokui.com/r/{name}.json"
     }
   }
   ```

   Without this step, the shadcn CLI looks up `@arbokui` in a public list that Arbokui does not control.

2. Add the theme once per app. It replaces your color tokens with ones checked for contrast:

   ```bash
   npx shadcn@latest add @arbokui/theme
   ```

3. Add the component:

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

## Use it for

- Picking one item from a long list, where typing is faster than scrolling: a country, a time zone, a teammate.
- One highlight glides from row to row as the pointer moves, and jumps when the arrow keys move it. Reduced motion makes it jump.
- Picking several items from a long list. Set multiple, and each pick shows as a chip in the field.
- Pass items as strings, as { value, label, disabled } objects, or as groups shaped { label, items }.
- The value is the item's string value: a string, or a string array when multiple is set. It works uncontrolled with defaultValue, even from a server component.
- Give the text field a name: aria-label, aria-labelledby, or an id that a label's htmlFor points at.
- Set name to send the value with a form. Multiple sends one entry per pick.
- The list opens at the end of the page body. In a box with its own theme, like a dark preview on a light page, pass that box as container so the list keeps its colors. A container that clips its overflow can clip the list.

## Avoid it for

- Fewer than about 7 items. Use a select or a radio group, so people see every choice at once.
- Free text, like a search box with suggestions. A combobox only accepts items from its list.
- Running commands or jumping to pages. Use a menu or links instead.
- Object values. Map each record to { value: id, label: name }, then look the record up by its id.

## Props

- `items`: `(string | { value: string; label: string; disabled?: boolean })[] | { label: string; items: ... }[]`. The choices. A string is both value and label. Groups show their label above their items.
- `multiple`: `boolean`. Default `false`. Pick any number of items. The value becomes a string array, and picks show as chips.
- `defaultValue`: `string | null | string[]`. The picked value at first render, when you do not control it. A string array when multiple is set.
- `value`: `string | null | string[]`. Controlled value. Use it with onValueChange.
- `onValueChange`: `(value: string | null | string[], details) => void`. Called with the new value on each pick. Deleting the text sets a single value to null.
- `aria-label`: `string`. Names the text field. TypeScript requires this, aria-labelledby, or id.
- `id`: `string`. The text field's id, for a label's htmlFor.
- `placeholder`: `string`. Hint text in the empty field. Hidden once a chip is in the field.
- `emptyText`: `ReactNode`. Default `"No results found."`. Shown in the list when nothing matches. Screen readers announce it.
- `size`: `"sm" | "md" | "lg"`. Default `"md"`. Field height, padding, and text size. The same heights as Input. Density scales them.
- `disabled`: `boolean`. Default `false`. Blocks typing and picking, and removes the field from the tab order.
- `readOnly`: `boolean`. Default `false`. Shows the value and the list but blocks changes.
- `required`: `boolean`. Default `false`. The form will not submit until something is picked.
- `name`: `string`. Form field name. The picked value is sent with the form.
- `aria-invalid`: `boolean`. Shows the danger border. Set it when validation fails, and link the message with aria-describedby.
- `filter`: `((item, query: string) => boolean) | null`. Decides which items match the text. The default matches labels, ignoring case and accents. Pass null when a server filters.
- `inputValue`: `string`. Controlled text. Use it with onInputValueChange, for example to search a server.
- `onInputValueChange`: `(inputValue: string) => void`. Called as the text changes, with the new text.
- `open`: `boolean`. Controlled open state. Use it with onOpenChange.
- `onOpenChange`: `(open: boolean) => void`. Called when the list opens or closes.
- `container`: `HTMLElement | ShadowRoot | RefObject<HTMLElement | ShadowRoot | null> | null`. Where the list opens. By default, at the end of the page body, so no parent can clip it. Pass an element, or a ref itself (not ref.current), to open it inside, for example to keep a parent's theme. While it is null, the list waits and does not open. Only client code has elements and refs.
- `effect`: `"rise" | "grow"`. Default `"rise"`. How the list opens and closes. rise fades in and moves out from the field. grow fades in and grows from the field, and closes the same way. Left out, it follows an effect-\<name> class on \<body>, where popups open, and rises with none.
- `className`: `string`. Classes for the field. Later classes win, so className='max-w-60' sets the width.

## Accessibility

- The text field has role="combobox". The list is a listbox, and each item is an option with aria-selected.
- Focus stays in the text field. aria-activedescendant tells screen readers which item is highlighted.
- Down Arrow opens the list on the first item. Up and Down Arrow move, Enter picks, and Escape closes. Typing filters.
- TypeScript requires aria-label, aria-labelledby, or an id. A label's htmlFor can name the field.
- The check mark is hidden from screen readers. aria-selected says which items are picked.
- The empty text is a polite live region, so screen readers hear when nothing matches.
- Group labels name their groups through aria-labelledby.
- With multiple, Left Arrow at the start of the text moves to the chips. Backspace or Delete removes the focused chip, and Backspace in empty text removes the last one.
- The open button is out of the tab order, so Tab moves on to the next control. Clicking anywhere in the field opens the list.
- The field shows the --ring outline while the text field inside has focus. The theme tests hold --ring to 3:1.
- Highlighted items use the accent pair, and group labels and empty text use muted text on the popover. The theme tests hold both to 4.5:1.
- The list fades in and slides by --motion-distance. It closes with a quicker fade and a slight shrink toward the field. Reduced motion keeps the fades and drops the slide and the shrink.
- The open button's label, 'Show options', and the chips' 'Remove …' labels are English. Edit the file to translate them.

## Examples

### Pick one item

```tsx
<Combobox
  aria-label="Time zone"
  placeholder="Find a time zone"
  items={["Europe/Lisbon", "Europe/Oslo", "America/New_York", "Asia/Tokyo"]}
/>
```

### With a label, a value, and a form name (needs @arbokui/label)

```tsx
import { Label } from "@/components/ui/label";

<div className="grid max-w-xs gap-1.5">
  <Label htmlFor="country">Country</Label>
  <Combobox
    id="country"
    name="country"
    defaultValue="pt"
    items={[
      { value: "no", label: "Norway" },
      { value: "pt", label: "Portugal" },
      { value: "se", label: "Sweden" },
    ]}
  />
</div>
```

### Groups

```tsx
<Combobox
  aria-label="Ingredient"
  items={[
    { label: "Fruit", items: ["Apple", "Banana", "Pear"] },
    { label: "Herbs", items: ["Basil", "Mint", "Thyme"] },
  ]}
/>
```

### Pick several, shown as chips

```tsx
<Combobox
  aria-label="Labels"
  multiple
  name="labels"
  placeholder="Add labels"
  defaultValue={["bug"]}
  items={[
    { value: "bug", label: "Bug" },
    { value: "docs", label: "Docs" },
    { value: "design", label: "Design" },
  ]}
/>
```

### Controlled

```tsx
"use client";
import { useState } from "react";

const people = [
  { value: "u1", label: "Ada Lovelace" },
  { value: "u2", label: "Grace Hopper" },
];

function AssigneePicker() {
  const [assignee, setAssignee] = useState<string | null>(null);
  return <Combobox aria-label="Assignee" items={people} value={assignee} onValueChange={setAssignee} />;
}
```

### Grows from the field

```tsx
<Combobox
  aria-label="Time zone"
  placeholder="Find a time zone"
  items={["Europe/Lisbon", "Europe/Oslo", "America/New_York", "Asia/Tokyo"]}
  effect="grow"
/>
```
