# Combobox

> A text input with a styled, filterable dropdown of suggestions, anchored under the input in the top layer.

URL: https://apogee.avaui.dev/docs/combobox
Last updated: 2026-10-15

A text input with a styled, filterable dropdown of suggestions, anchored under the input in the top layer.

Last updated October 15, 2026

Uses JavaScript

Wires a real ARIA combobox: typing filters the list and highlights the closest match (prefers a prefix match); arrow keys move the highlight and wrap, Enter or a click commits it into the input. Free-typing is always allowed.

**Without JS:** The listbox is a closed popover and never appears — the field still works as a plain text input, free-typed and submitted under its \`name\`.

```astro
<Combobox label="Country" options={['Canada', 'France', 'Germany', 'Japan']} />
```

## Add

```sh
npx @ava-ui/apogee add combobox
```

Pulls in `Field` (label, hint, error scaffold) and `utils`. Uses `@lucide/astro` internally (`ChevronDown`) for the caret — no icon import needed in your usage.

## Usage

```astro
---
import Combobox from '@/components/apogee/Combobox/Combobox.astro'
---
<Combobox label="Country" options={['Canada', 'France', 'Germany', 'Japan']} />
<Combobox label="Assignee" name="assignee" options={[
  { value: 'u1', label: 'Ada Lovelace', description: 'Engineering' },
  { value: 'u2', label: 'Grace Hopper', description: 'Research', disabled: true },
]} />
```

## Anatomy

-   **Label / hint / error** — from `Field`.
-   **Input** — the visible text field, `role="combobox"`.
-   **Caret** — optional chevron icon (toggle with `caret`).
-   **Listbox** — a `role="listbox"` popover of `role="option"` suggestions, anchored under the input and as wide as it, filtered as you type; scrolls past 15rem.

## API

`@composable` — exposed to Content Editors in AVA. Any other attribute passes through to the input (e.g. `placeholder`, `disabled`, `name`).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | auto-generated | Input id; drives the listbox id (`${id}-list`) and `Field` wiring. |
| `label` | `string` | — | Field label. |
| `hint` | `string` | — | Helper text (hidden when `error` is set). |
| `error` | `string` | — | Error text; sets `aria-invalid` and renders via `role="alert"`. |
| `options` | `(string \| { value, label?, description?, disabled?, icon?, avatar?, avatarName? })[]` | — | Suggestions. A string is both value and label; an object can carry a different `value`, a `description` line, `disabled`, and media before the label (a Lucide component, or an image URL with `avatarName` for the fallback). Required. |
| `size` | `'small' \| 'medium' \| 'large' \| 'xlarge'` | `'medium'` | Height: 28 / 36 / 48 / 56px. |
| `variant` | `'outline' \| 'faded' \| 'ghost' \| 'headless'` | `'outline'` | The box style: bordered, tinted, bare, or no box at all. |
| `hasError` | `boolean` | `false` | Marks the field invalid (`aria-invalid`, critical border) without an `error` message. |
| `required` | `boolean` | `false` | Marks the input required (native + `Field`’s `*` indicator). |
| `caret` | `boolean` | `true` | Shows the chevron caret. Set `false` to hide it. |
| `multiple` | `boolean` | `false` | Picks become removable chips inside the field; each posts `name=value` through a hidden input, chosen options leave the list, Backspace on an empty input removes the last chip. |
| `name` | `string` | — | Form field name (required with `multiple`). |
| `value` | `string \| string[]` | — | Initial text (single), or initial chips as option values (`multiple`). |
| `maxItems` | `number` | — | Stops accepting picks at this count. |
| `class` | `string` | — | Extra classes, passed to the `Field` wrapper. |

## Rich options

Objects give each suggestion its own `value` (sent in the select event), a description line, and a `disabled` state the arrow keys skip.

```astro
<Combobox label="Assignee" name="assignee" options={[
{ value: 'u1', label: 'Ada Lovelace', description: 'Engineering' },
{ value: 'u2', label: 'Grace Hopper', description: 'Research' },
{ value: 'u3', label: 'Katherine Johnson', description: 'On leave', disabled: true },
{ value: 'u4', label: 'Margaret Hamilton', description: 'Flight software' },
]} />
```

## Icons and avatars

```astro
<Combobox label="Mention" name="mention" options={[
{ value: 'u1', label: 'Ana Lima', avatar: 'https://i.pravatar.cc/48?img=5', avatarName: 'Ana Lima' },
{ value: 'team', label: 'Whole team', icon: Zap },
]} />
```

## Multiple

```astro
<form>
<Combobox label="Reviewers" name="reviewers" multiple placeholder="Add a reviewer" options={[
  { value: 'ana', label: 'Ana Lima' }, { value: 'ben', label: 'Ben Ortiz' }, { value: 'cy', label: 'Cy Park' },
]} />
</form>
```

Without JS the typing field posts free text under `name`.

```astro
<Combobox label="Country" name="country" value="France" options={['Canada', 'France', 'Japan']} />
<form><Combobox label="Owners" name="owners" multiple required value={['ana']} options={[{ value: 'ana', label: 'Ana Lima' }, { value: 'ben', label: 'Ben Ortiz' }]} /></form>
```

## Long list and clipping

The listbox scrolls past 15rem and sits in the top layer, so an `overflow: hidden` ancestor cannot cut it off.

```astro
<div class="w-64 overflow-hidden rounded-md border border-neutral p-4">
<Combobox label="Month" options={['January', 'February', …]} />
</div>
```

## Sizes

```astro
<Combobox label="Small" size="small" options={['One', 'Two', 'Three']} />
<Combobox label="Medium" options={['One', 'Two', 'Three']} />
<Combobox label="Large" size="large" options={['One', 'Two', 'Three']} />
<Combobox label="Extra large" size="xlarge" options={['One', 'Two', 'Three']} />
```

## States

-   **Default** — closed, empty or pre-filled input.
-   **Open** — focusing or typing in the input opens the listbox; the closest match is highlighted (`aria-selected` + `aria-activedescendant`).
-   **Error** — critical border, `aria-invalid="true"`, error text replaces the hint.
-   **Disabled** — via passthrough `disabled`; dimmed, not interactive.
-   **No JS** — the listbox never opens; the input is a plain text field.

```astro
<Combobox label="Country" options={['Canada', 'France']} error="Pick a country from the list." />
<Combobox label="Country" options={['Canada', 'France']} disabled value="Canada" />
```

## Examples

Without the caret:

```astro
<Combobox label="Tag" options={['bug', 'feature', 'docs']} caret={false} />
```

Required, wired into a form with a submit name:

```astro
<Combobox label="Country" name="country" required options={['Canada', 'France', 'Germany']} />
```

## Events

Committing an option (Enter or click) writes its label into the input and dispatches native `input` and `change` events plus `apogee:combobox-select` (`detail: { value, label }`, bubbles) on the root, so forms and listeners see the selection. The input submits the label; read `value` from the event when the two differ.

## Accessibility

| Key | Action |
| --- | --- |
| `↓` | Opens the list if closed, then moves the highlight down; wraps. |
| `↑` | Moves the highlight up; wraps. |
| `Enter` | Commits the highlighted option. |
| `Esc` | Closes the list. |
| `Backspace` | Removes the last chip when the input is empty (`multiple` only). |
| `Tab` | Moves focus out and closes the list. |

-   Full ARIA combobox pattern: `role="combobox"` on the input, wired with `aria-expanded`, `aria-controls`, `aria-autocomplete="list"`, and `aria-activedescendant` on the highlighted option; `role="listbox"` / `role="option"` with `aria-selected` on the dropdown; disabled options carry `aria-disabled`.
-   Clicking an option commits it without losing input focus.
-   Focus leaving the whole widget (tab away, click elsewhere) closes the list.
-   **No JS:** the listbox is a closed popover and never opens — the input still works as a plain, free-typed text field that submits under its `name`.

## Tokens

| Token | Used for |
| --- | --- |
| `--border-neutral` | Input and listbox border |
| `--background-page` | Input background |
| `--background-elevation-overlay` | Listbox panel background |
| `--foreground-neutral` | Input and option text |
| `--foreground-neutral-faded` | Placeholder text, caret, descriptions |
| `--foreground-disabled` | Disabled options |
| `--background-neutral-faded` | Highlighted option background |
| `--background-neutral-faded-highlighted` | Option hover |
| `--border-critical` | Error border |
| `--border-primary` | Focus border |
| `--duration-fast` / `--ease-standard` | Listbox open fade/scale |

## Do / Don’t

-   **Do** keep `options` short and relevant — it’s a suggestion list, not a full dataset; filter server-side before passing in a large set.
-   **Do** allow free text — Combobox never forces a value from `options`.
-   **Don’t** use Combobox when the value must be constrained to the list — use `Select` instead.
-   **Don’t** rely on `disabled` to lock in a submitted value — a disabled input doesn’t submit at all.

## Content

-   Label names the field (“Country”), not the widget (“Combobox”).
-   Error copy states what’s wrong or what format is expected.

## Related

[

SelectA labeled, native <select> — full keyboard and mobile picker support for free — or a custom listbox with rich options over the same form control.

](https://apogee.avaui.dev/docs/select)[

TagInputType and press Enter to add removable tags. Submits as repeated hidden inputs.

](https://apogee.avaui.dev/docs/tag-input)[

CommandPaletteA ⌘K / Ctrl+K searchable command menu in a native dialog.

](https://apogee.avaui.dev/docs/command-palette)
