# Combobox

> Searchable select for long lists, single or multiple (chips), with Arabic-aware filtering and an empty state. Wraps Base UI Combobox.

Source: https://docs.nasaqui.com/components/combobox

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/combobox.json
```

A form control for choosing from a long list by typing. It looks like a `Select`, filters as you type, and in
`multiple` mode shows the selection as removable chips. Filtering folds Arabic letter variants and diacritics,
so typing `اداره` finds `إدارة`.

## When to use

- One or many choices from a list too long to scan (countries, people, tags).

## When not to use

- Four to fifteen options: use [`Select`](https://docs.nasaqui.com/components/select).
- Free text with no fixed options: use [`Input`](https://docs.nasaqui.com/components/field).

## Import

```tsx
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  Field,
  FieldLabel,
} from "@fadymondy/nasaq/web";

const COUNTRIES = [
  { value: "sa", label: "Saudi Arabia" },
  { value: "eg", label: "Egypt" },
  { value: "jo", label: "Jordan" },
];

export function CountryField() {
  return (
    <Field>
      <FieldLabel>Country</FieldLabel>
      <Combobox items={COUNTRIES}>
        <ComboboxInput placeholder="Search a country…" />
        <ComboboxContent>
          <ComboboxEmpty>No results</ComboboxEmpty>
          <ComboboxList>
            {(item: { value: string; label: string }) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}
```

## Anatomy

```
Combobox                  Base UI Combobox.Root (filter defaults to comboboxFilter)
├─ ComboboxInput          single: box with input, clear and chevron   data-slot="combobox-input-group"
│  or ComboboxChips       multiple: chips + inline input              data-slot="combobox-chips"
│     └─ chip             data-slot="combobox-chip" / "combobox-chip-remove"
└─ ComboboxContent        portal + positioner + popup                 data-slot="combobox-content"
   ├─ ComboboxEmpty       data-slot="combobox-empty"
   └─ ComboboxList        data-slot="combobox-list"
      ├─ ComboboxGroup / ComboboxLabel / ComboboxSeparator
      └─ ComboboxItem     data-slot="combobox-item", check at the inline-start
```

## API

**Combobox**: Base UI `Combobox.Root` props (`items`, `value`, `defaultValue`, `onValueChange`, `multiple`,
`name`, `disabled`, `itemToStringLabel`, `isItemEqualToValue`, `inputValue`, ...). Items shaped `{ value, label }`
need no extra config. Differences from Base UI:

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `filter` | `null \| (item, query, itemToString?) => boolean` | `comboboxFilter` | Arabic-aware substring match. `null` turns filtering off (async search). |

**ComboboxInput** (single mode)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `clearable` | `boolean` | `true` | Show a clear button when there is a value. |
| `clearLabel` | `string` | `"Clear"` | Accessible name of the clear button. Localise. |
| `triggerLabel` | `string` | `"Open"` | Accessible name of the chevron button. Localise. |

Other props go to the Base UI `Combobox.Input` (`placeholder`, `id`, `aria-invalid`).

**ComboboxChips** (multiple mode)

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` | none | Shown only while nothing is selected. |
| `itemToLabel` | `(item: unknown) => ReactNode` | item `label` | Chip text. |
| `removeLabel` | `string` | `"Remove"` | Accessible name of each remove button. Localise. |
| `inputProps` | `Combobox.Input` props | none | Props for the inline input. |

**ComboboxContent**: `side` (`"bottom"`), `align` (`"start"`), `sideOffset` (`4`), plus Base UI `Popup` props.
In multi mode the popup is anchored to the chips box and matches its width.

**ComboboxList**: takes a render function `(item) => ReactNode` over the filtered items, or plain children.

**ComboboxItem**: `value` is the item object; `disabled` is supported. **ComboboxEmpty** shows only when the
filter leaves nothing and requires `items` on the root.

**comboboxFilter(item, query, itemToString?)**: the default filter, exported for reuse.

## Examples

Multiple with chips, controlled, Arabic copy:

```tsx
import {
  Combobox,
  ComboboxChips,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
} from "@fadymondy/nasaq/web";
import { useState } from "react";

type Option = { value: string; label: string };
const ITEMS: Option[] = [
  { value: "sa", label: "المملكة العربية السعودية" },
  { value: "eg", label: "مصر" },
  { value: "jo", label: "الأردن" },
];

export function Countries() {
  const [value, setValue] = useState<Option[]>([]);
  return (
    <Combobox multiple items={ITEMS} value={value} onValueChange={setValue}>
      <ComboboxChips placeholder="ابحث عن دولة…" removeLabel="إزالة" />
      <ComboboxContent>
        <ComboboxEmpty>لا توجد نتائج</ComboboxEmpty>
        <ComboboxList>
          {(item: Option) => (
            <ComboboxItem key={item.value} value={item}>
              {item.label}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  );
}
```

## Accessibility

| Key | Action |
| --- | --- |
| Down / Up | Opens the list and moves the highlight. |
| Enter | Selects the highlighted item. |
| Escape | Closes the list. |
| Backspace (multi, empty input) | Highlights then removes the last chip. |
| Left / Right (multi) | Moves between chips. |

The input has `role="combobox"` and the list `role="listbox"`; the empty state is announced politely. The
clear, chevron and chip-remove buttons are icon-only: pass localised `clearLabel`, `triggerLabel`, `removeLabel`.
Label the control with `FieldLabel`.

## RTL & i18n

Padding, the check indicator and chip layout use logical properties and mirror in RTL. Filtering folds case,
diacritics, tatweel and alef/yeh/teh-marbuta variants. Localise the empty message and the button labels.

## Styling & tokens

Uses `border-input`, `bg-card`, `border-nq-focus`, `border-nq-danger`, `h-control`, `rounded-control`,
`bg-popover`, `shadow-floating`, `bg-nq-selected`. State attributes: `data-highlighted`, `data-disabled`,
`data-invalid`. Extend with `className`.

## Do / Don't

- Do provide an empty state. Do label the control.
- Don't use it for fewer than ~5 options; use `Select`.

## Related

- [Select](https://docs.nasaqui.com/components/select)
- [Field](https://docs.nasaqui.com/components/field)
- [InputGroup](https://docs.nasaqui.com/components/input-group)

## Lab

`https://docs.nasaqui.com/?path=/docs/components-forms-combobox--docs`

## Code

### React

```tsx
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  Field,
  FieldLabel,
} from "@fadymondy/nasaq/web";

const COUNTRIES = [
  { value: "sa", label: "Saudi Arabia" },
  { value: "eg", label: "Egypt" },
  { value: "jo", label: "Jordan" },
];

export function CountryField() {
  return (
    <Field>
      <FieldLabel>Country</FieldLabel>
      <Combobox items={COUNTRIES}>
        <ComboboxInput placeholder="Search a country…" />
        <ComboboxContent>
          <ComboboxEmpty>No results</ComboboxEmpty>
          <ComboboxList>
            {(item: { value: string; label: string }) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}
```

### shadcn

```tsx
import { Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput, ComboboxItem, ComboboxList } from "@/components/ui/combobox";
import { Field, FieldLabel } from "@/components/ui/field";

const COUNTRIES = [
  { value: "sa", label: "Saudi Arabia" },
  { value: "eg", label: "Egypt" },
  { value: "jo", label: "Jordan" },
];

export function CountryField() {
  return (
    <Field>
      <FieldLabel>Country</FieldLabel>
      <Combobox items={COUNTRIES}>
        <ComboboxInput placeholder="Search a country…" />
        <ComboboxContent>
          <ComboboxEmpty>No results</ComboboxEmpty>
          <ComboboxList>
            {(item: { value: string; label: string }) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import {
  NqCombobox,
  NqComboboxContent,
  NqComboboxEmpty,
  NqComboboxInput,
  NqComboboxItem,
  NqComboboxList,
  NqField,
  NqFieldLabel,
} from "@fadymondy/nasaq/vue";

const COUNTRIES = [
  { value: "sa", label: "Saudi Arabia" },
  { value: "eg", label: "Egypt" },
  { value: "jo", label: "Jordan" },
];
</script>

<template>
  <NqField>
    <NqFieldLabel>Country</NqFieldLabel>
    <NqCombobox :items="COUNTRIES">
      <NqComboboxInput placeholder="Search a country…" />
      <NqComboboxContent>
        <NqComboboxEmpty>No results</NqComboboxEmpty>
        <NqComboboxList v-slot="{ items }">
          <NqComboboxItem v-for="item in items as typeof COUNTRIES" :key="item.value" :value="item">{{ item.label }}</NqComboboxItem>
        </NqComboboxList>
      </NqComboboxContent>
    </NqCombobox>
  </NqField>
</template>
```

### Blade

```blade
<div class="flex w-80 flex-col gap-6">
    <div class="flex flex-col gap-1.5">
        <span class="text-label text-foreground">Country</span>
        <x-nq::combobox name="country" value="eg">
            <x-nq::combobox.input placeholder="Search a country…" aria-label="Country" />
            <x-nq::combobox.content>
                <x-nq::combobox.empty>No results</x-nq::combobox.empty>
                <x-nq::combobox.list>
                    <x-nq::combobox.item value="sa">Saudi Arabia</x-nq::combobox.item>
                    <x-nq::combobox.item value="eg">Egypt</x-nq::combobox.item>
                    <x-nq::combobox.item value="jo">Jordan</x-nq::combobox.item>
                </x-nq::combobox.list>
            </x-nq::combobox.content>
        </x-nq::combobox>
    </div>
    <div class="flex flex-col gap-1.5">
        <span class="text-label text-foreground">Markets</span>
        <x-nq::combobox name="markets" :value="['sa']" multiple>
            <x-nq::combobox.chips placeholder="Pick markets" aria-label="Markets" />
            <x-nq::combobox.content>
                <x-nq::combobox.empty>No results</x-nq::combobox.empty>
                <x-nq::combobox.list>
                    <x-nq::combobox.item value="sa">Saudi Arabia</x-nq::combobox.item>
                    <x-nq::combobox.item value="eg">Egypt</x-nq::combobox.item>
                    <x-nq::combobox.item value="jo">Jordan</x-nq::combobox.item>
                </x-nq::combobox.list>
            </x-nq::combobox.content>
        </x-nq::combobox>
    </div>
</div>
```

### HTML + Alpine

```html
<div class="flex w-80 flex-col gap-6">
    <div class="flex flex-col gap-1.5">
        <span class="text-label text-foreground">Country</span>
        <div data-slot="combobox" x-data="nqCombobox('eg', false)" x-modelable="value" x-id="['nq-combobox']" class="contents">
    <div data-slot="combobox-input-group" x-ref="anchor"
    class="flex min-h-control min-w-0 w-full items-center gap-1 rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 h-control ps-3 pe-1.5">
    <input data-slot="combobox-input" x-ref="input" x-bind="input"
                placeholder="Search a country…" aria-label="Country" class="h-full min-w-0 flex-1 border-0 bg-transparent text-body text-foreground outline-none placeholder:text-muted-foreground pointer-coarse:text-[16px]">
            <button data-slot="combobox-clear" x-bind="clear" aria-label="Clear"
            class="flex size-6 shrink-0 cursor-default items-center justify-center rounded-control text-muted-foreground outline-none hover:text-foreground focus-visible:outline-1 focus-visible:outline-nq-focus [&_svg]:size-4 data-[hidden]:hidden"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
        <button data-slot="combobox-trigger" x-bind="trigger" aria-label="Open"
        class="flex size-6 shrink-0 cursor-default items-center justify-center rounded-control text-muted-foreground outline-none hover:text-foreground focus-visible:outline-1 focus-visible:outline-nq-focus [&_svg]:size-4"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg></button>
</div>
            <template x-teleport="body">
    <div data-slot="combobox-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.anchor"
        class="z-50 w-[var(--anchor-width)] max-h-[min(var(--available-height),20rem)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="combobox-empty" x-bind="emptyState" class="px-2.5 py-2 text-body-sm text-muted-foreground empty:hidden">No results</div>
                <div data-slot="combobox-list" class="outline-none"><div data-slot="combobox-item" x-bind="item('sa', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('sa')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Saudi Arabia</span>
</div>
                    <div data-slot="combobox-item" x-bind="item('eg', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('eg')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Egypt</span>
</div>
                    <div data-slot="combobox-item" x-bind="item('jo', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('jo')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Jordan</span>
</div></div>
    </div>
</template>
            <template x-if="multiple">
            <template x-for="v in value" :key="v"><input type="hidden" name="country[]" :value="v"></template>
        </template>
        <template x-if="!multiple"><input type="hidden" name="country" :value="value ?? ''"></template>
    </div>
    </div>
    <div class="flex flex-col gap-1.5">
        <span class="text-label text-foreground">Markets</span>
        <div data-slot="combobox" x-data="nqCombobox(JSON.parse('[\u0022sa\u0022]'), true)" x-modelable="value" x-id="['nq-combobox']" class="contents">
    <div data-slot="combobox-chips" x-ref="anchor"
    class="flex min-h-control min-w-0 w-full items-center gap-1 rounded-control border border-input bg-card text-body text-foreground transition-colors duration-150 ease-nq focus-within:border-nq-focus focus-within:outline-1 focus-within:outline-nq-focus has-[[data-invalid]]:border-nq-danger has-[[aria-invalid=true]]:border-nq-danger has-[input:disabled]:cursor-not-allowed has-[input:disabled]:opacity-50 flex-wrap px-1.5 py-1">
    <template x-for="v in selected()" :key="v">
        <span data-slot="combobox-chip" class="inline-flex h-6 max-w-full items-center gap-1 rounded-[4px] border border-border bg-secondary ps-2 pe-0.5 text-body-sm text-foreground outline-none data-highlighted:border-nq-focus">
            <span class="min-w-0 truncate" x-text="labelOf(v)"></span>
            <button type="button" data-slot="combobox-chip-remove" tabindex="-1" :aria-label="'Remove' + ' ' + labelOf(v)" @click.stop="remove(v)"
                class="flex size-5 shrink-0 cursor-default items-center justify-center rounded-[4px] text-muted-foreground outline-none hover:text-foreground [&_svg]:size-3"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M18 6 6 18"/>
  <path d="m6 6 12 12"/>
</svg></button>
        </span>
    </template>
    <input data-slot="combobox-input" x-ref="input" x-bind="input" :placeholder="hasValue() ? undefined : 'Pick markets'"
                aria-label="Markets" class="h-full min-w-0 flex-1 border-0 bg-transparent text-body text-foreground outline-none placeholder:text-muted-foreground pointer-coarse:text-[16px]">
</div>
            <template x-teleport="body">
    <div data-slot="combobox-content" x-ref="popup" x-bind="popup" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.anchor"
        class="z-50 w-[var(--anchor-width)] max-h-[min(var(--available-height),20rem)] overflow-y-auto rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0">
        <div data-slot="combobox-empty" x-bind="emptyState" class="px-2.5 py-2 text-body-sm text-muted-foreground empty:hidden">No results</div>
                <div data-slot="combobox-list" class="outline-none"><div data-slot="combobox-item" x-bind="item('sa', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('sa')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Saudi Arabia</span>
</div>
                    <div data-slot="combobox-item" x-bind="item('eg', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('eg')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Egypt</span>
</div>
                    <div data-slot="combobox-item" x-bind="item('jo', false)"
    class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control ps-8 pe-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50">
    <span aria-hidden="true" class="absolute start-2.5 inline-flex size-4 items-center justify-center">
        <span x-show="isSelected('jo')" x-cloak class="contents"><svg class="size-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span>
    </span>
    <span data-slot="combobox-item-text" class="min-w-0 flex-1 truncate">Jordan</span>
</div></div>
    </div>
</template>
            <template x-if="multiple">
            <template x-for="v in value" :key="v"><input type="hidden" name="markets[]" :value="v"></template>
        </template>
        <template x-if="!multiple"><input type="hidden" name="markets" :value="value ?? ''"></template>
    </div>
    </div>
</div>
```
