# Combobox (/ui/components/react/combobox)



<Playground component="combobox" />

## Usage [#usage]

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
```

```tsx
const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart']

<Combobox items={frameworks}>
  <ComboboxInput placeholder="Search a framework" />
  <ComboboxContent>
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
```

`Combobox` is a compound component. The root holds the data (`items`) and shared options (`size`, `variant`, `clearable`, `icon`, `multiple`, `grid`); the parts compose the input and the filtered popup:

* **`ComboboxInput`** — the text field, styled like [`Input`](/ui/components/react/input). It filters the list as you type. Add adornments with `startSlot` / `endSlot`.
* **`ComboboxTrigger`*&#x2A; / &#x2A;*`ComboboxValue`** — a select-style trigger (also styled like `Input`) that shows the current value and opens the popup. Use it instead of `ComboboxInput` when the search field should live **inside** the popup (see [Country select](#country-select)).
* **`ComboboxChips`*&#x2A; / &#x2A;*`ComboboxChip`** — a chip-based input for `multiple` selection, in place of `ComboboxInput`.
* **`ComboboxContent`** — the portalled, auto-positioned popup. Accepts the shared [floating props](/ui/components/react/popover).
* **`ComboboxList`** — the scrollable list. Pass a render function over the filtered items, or static `ComboboxItem`s.
* **`ComboboxItem`** — one option; its `value` is what selection reports.
* **`ComboboxEmpty`** — shown only when the filter matches nothing.

`Combobox` and [`Autocomplete`](/ui/components/react/autocomplete) are siblings built on the same Base UI primitive. Reach for `Combobox` when you're **picking a value** from a set (its chevron, clearable, chips, and grid lean toward selection); use `Autocomplete` for a leaner typeahead. For a short, known list with no search, use a [`Select`](/ui/components/react/select).

## Examples [#examples]

### Variants [#variants]

`variant` switches the input appearance between a bordered `outline` (default) and a filled `soft` field.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']
const variants = ['outline', 'soft'] as const

export default function ComboboxVariants() {
  return (
    <div className="flex w-full max-w-70 flex-col gap-4">
      {variants.map((variant) => (
        <Combobox key={variant} items={frameworks} variant={variant}>
          <ComboboxInput placeholder="Search a framework" aria-label={`Search (${variant})`} />
          <ComboboxContent>
            <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      ))}
    </div>
  )
}
```

### Sizes [#sizes]

`size` on the root scales the input, popup radius, and items together — `sm`, `md` (default), or `lg`.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']
const sizes = ['sm', 'md', 'lg'] as const

export default function ComboboxSizes() {
  return (
    <div className="flex w-full max-w-70 flex-col gap-4">
      {sizes.map((size) => (
        <Combobox key={size} items={frameworks} size={size}>
          <ComboboxInput placeholder="Search a framework" aria-label={`Search (${size})`} />
          <ComboboxContent>
            <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      ))}
    </div>
  )
}
```

### Clearable & chevron [#clearable--chevron]

`icon` (a chevron that toggles the popup) is **on by default**; turn it off with `icon={false}`. Set `clearable` to add a clear (✕) button once there's a value. Both render inside the input.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']

export default function ComboboxClearable() {
  return (
    <div className="w-full max-w-70">
      <Combobox items={frameworks} defaultValue="Next.js" clearable>
        <ComboboxInput placeholder="Search a framework" aria-label="Search a framework" />
        <ComboboxContent>
          <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Disabled & error states [#disabled--error-states]

`disabled` on the root greys out the input and blocks interaction. Set `aria-invalid` on the `ComboboxInput` to paint the error state — it's mirrored to `data-invalid`. Inside a [`Field`](/ui/components/react/field), the input also inherits the field's `disabled` and `invalid` state, which is the better choice when you're rendering a validation message.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']

export default function ComboboxStates() {
  return (
    <div className="flex w-full max-w-70 flex-col gap-4">
      <Combobox items={frameworks} disabled>
        <ComboboxInput placeholder="Disabled" aria-label="Disabled" />
        <ComboboxContent>
          <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>

      <Combobox items={frameworks} defaultValue="Next.js">
        <ComboboxInput aria-invalid placeholder="Search a framework" aria-label="With error" />
        <ComboboxContent>
          <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Start & end slots [#start--end-slots]

`ComboboxInput` accepts `startSlot` and `endSlot` for adornments inside the input frame — a search icon, a shortcut hint, a unit — laid out around the field and its controls.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
import { Search } from '@appica/icons-react'

const commands = ['Open file', 'New window', 'Toggle sidebar', 'Find in files', 'Go to line', 'Command palette']

export default function ComboboxSlots() {
  return (
    <div className="w-full max-w-70">
      <Combobox items={commands} icon={false}>
        <ComboboxInput
          placeholder="Search commands"
          aria-label="Search commands"
          startSlot={<Search className="text-foreground-muted size-4.5" />}
          endSlot={<span className="text-foreground-subtle text-xs">⌘K</span>}
        />
        <ComboboxContent>
          <ComboboxEmpty>No commands found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Country select [#country-select]

A searchable country picker with [flags](/ui/country-flags). When `items` are objects, give the root an `itemToStringLabel` so the input and filter know how to read each one; here the selected country's flag is mirrored into the input through `startSlot`. The list is long, so its `ComboboxList` is wrapped in a [`ScrollArea`](/ui/components/react/scroll-area) for a custom, themeable scrollbar (set `overflow-y-visible` on the list so the `ScrollArea` owns the scroll).

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
import { ScrollArea } from '@appica/ui-react/scroll-area'
import { CountryFlagRounded } from '@appica/country-flags-react'
import { World } from '@appica/icons-react'

interface Country {
  code: string
  name: string
}

const countries: Country[] = [
  { code: 'ar', name: 'Argentina' },
  { code: 'au', name: 'Australia' },
  { code: 'be', name: 'Belgium' },
  { code: 'br', name: 'Brazil' },
  { code: 'ca', name: 'Canada' },
  { code: 'cn', name: 'China' },
  { code: 'dk', name: 'Denmark' },
  { code: 'eg', name: 'Egypt' },
  { code: 'fr', name: 'France' },
  { code: 'de', name: 'Germany' },
  { code: 'in', name: 'India' },
  { code: 'it', name: 'Italy' },
  { code: 'jp', name: 'Japan' },
  { code: 'mx', name: 'Mexico' },
  { code: 'nl', name: 'Netherlands' },
  { code: 'no', name: 'Norway' },
  { code: 'pt', name: 'Portugal' },
  { code: 'kr', name: 'South Korea' },
  { code: 'es', name: 'Spain' },
  { code: 'se', name: 'Sweden' },
  { code: 'ch', name: 'Switzerland' },
  { code: 'tr', name: 'Türkiye' },
  { code: 'gb', name: 'United Kingdom' },
  { code: 'us', name: 'United States' },
]

export default function ComboboxCountry() {
  const [selected, setSelected] = React.useState<Country | null>(null)

  return (
    <div className="w-full max-w-70">
      <Combobox
        items={countries}
        value={selected}
        onValueChange={(v) => setSelected(v as Country | null)}
        itemToStringLabel={(item) => (item as Country).name}
        clearable
      >
        <ComboboxInput
          placeholder="Select a country"
          aria-label="Select a country"
          startSlot={
            selected ? (
              <CountryFlagRounded code={selected.code} aria-hidden />
            ) : (
              <World className="text-foreground-muted" />
            )
          }
        />
        <ComboboxContent>
          <ComboboxEmpty>No countries found.</ComboboxEmpty>
          <ScrollArea className="max-h-72">
            <ComboboxList className="overflow-y-visible">
              {(country: Country) => (
                <ComboboxItem key={country.code} value={country}>
                  <CountryFlagRounded code={country.code} data-icon="start" aria-hidden />
                  {country.name}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ScrollArea>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Input inside popup and custom scroll [#input-inside-popup-and-custom-scroll]

For a select-style picker, render a `ComboboxTrigger` (showing the current `ComboboxValue`) as the field and move the search `ComboboxInput` **inside** `ComboboxContent`. Set `icon={false}` on the root so the in-popup input drops its own chevron — the trigger already has one. As with [Country select](#country-select), the long list is wrapped in a [`ScrollArea`](/ui/components/react/scroll-area) for a custom scrollbar.

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxTrigger,
  ComboboxValue,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
import { ScrollArea } from '@appica/ui-react/scroll-area'

const languages = [
  'Arabic',
  'Bengali',
  'Chinese',
  'Czech',
  'Danish',
  'Dutch',
  'English',
  'Finnish',
  'French',
  'German',
  'Greek',
  'Hebrew',
  'Hindi',
  'Hungarian',
  'Indonesian',
  'Italian',
  'Japanese',
  'Korean',
  'Norwegian',
  'Polish',
  'Portuguese',
  'Romanian',
  'Russian',
  'Spanish',
  'Swedish',
  'Thai',
  'Turkish',
  'Ukrainian',
  'Vietnamese',
]

export default function ComboboxPopupSearch() {
  const [value, setValue] = React.useState<string | null>(null)

  return (
    <div className="w-full max-w-70">
      <Combobox items={languages} value={value} onValueChange={(v) => setValue(v as string | null)} icon={false}>
        <ComboboxTrigger aria-label="Select a language">
          <ComboboxValue placeholder="Select a language" />
        </ComboboxTrigger>
        <ComboboxContent>
          <div className="px-2 pb-2 group-data-empty/combobox-content:pt-2">
            <ComboboxInput placeholder="Search a language" aria-label="Search a language" />
          </div>
          <ScrollArea className="max-h-64">
            <ComboboxEmpty>No languages found.</ComboboxEmpty>
            <ComboboxList className="overflow-y-visible">
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ScrollArea>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### User select [#user-select]

Rich items with an [`Avatar`](/ui/components/react/avatar), a name, and a trailing role — search a directory and assign a person. The chosen avatar is echoed back into the input via `startSlot`.

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
import { Avatar, AvatarImage, AvatarFallback } from '@appica/ui-react/avatar'
import { UserFilled } from '@appica/icons-react'

interface User {
  id: string
  name: string
  role: string
  avatar: string
  initials: string
}

const users: User[] = [
  { id: 'liam', name: 'Liam Hudson', role: 'Admin', avatar: '/avatars/01.jpg', initials: 'LH' },
  { id: 'sarah', name: 'Sarah Jenkins', role: 'User', avatar: '/avatars/02.jpg', initials: 'SJ' },
  { id: 'mateo', name: 'Mateo Rossi', role: 'User', avatar: '/avatars/03.jpg', initials: 'MR' },
  { id: 'ava', name: 'Ava Thompson', role: 'Editor', avatar: '/avatars/04.jpg', initials: 'AT' },
  { id: 'noah', name: 'Noah Patel', role: 'User', avatar: '/avatars/05.jpg', initials: 'NP' },
  { id: 'emma', name: 'Emma Garcia', role: 'User', avatar: '/avatars/06.jpg', initials: 'EG' },
  { id: 'lucas', name: 'Lucas Müller', role: 'Admin', avatar: '/avatars/07.jpg', initials: 'LM' },
  { id: 'olivia', name: 'Olivia Bennett', role: 'User', avatar: '/avatars/08.jpg', initials: 'OB' },
]

export default function ComboboxUsers() {
  const [selected, setSelected] = React.useState<User | null>(null)

  return (
    <div className="w-full max-w-70">
      <Combobox
        items={users}
        value={selected}
        onValueChange={(v) => setSelected(v as User | null)}
        itemToStringLabel={(item) => (item as User).name}
        clearable
      >
        <ComboboxInput
          placeholder="Assign to…"
          aria-label="Assign to"
          startSlot={
            <Avatar size={22}>
              {selected && <AvatarImage src={selected.avatar} alt={selected.name} />}
              <AvatarFallback>{selected ? selected.initials : <UserFilled />}</AvatarFallback>
            </Avatar>
          }
        />
        <ComboboxContent>
          <ComboboxEmpty>No people found.</ComboboxEmpty>
          <ComboboxList>
            {(user: User) => (
              <ComboboxItem key={user.id} value={user} className="[&>span:first-child]:flex-1">
                <Avatar size={22} data-icon="start">
                  <AvatarImage src={user.avatar} alt={user.name} />
                  <AvatarFallback>{user.initials}</AvatarFallback>
                </Avatar>
                {user.name}
                <span className="text-foreground-muted ms-auto ps-3 text-xs">{user.role}</span>
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Grouped options [#grouped-options]

Pass a grouped `items` array — `{ value, items }[]` — and render each group with `ComboboxGroup` + `ComboboxLabel` + `ComboboxCollection`. The outer render function iterates groups; `ComboboxCollection` renders the items inside each. As with [Country select](#country-select), this longer list wraps its `ComboboxList` in a [`ScrollArea`](/ui/components/react/scroll-area) for a custom scrollbar.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxCollection,
} from '@appica/ui-react/combobox'
import { ScrollArea } from '@appica/ui-react/scroll-area'

interface ProduceGroup {
  value: string
  items: string[]
}

const groups: ProduceGroup[] = [
  { value: 'Fruits', items: ['Apple', 'Banana', 'Cherry', 'Grapefruit', 'Mango', 'Orange', 'Strawberry'] },
  { value: 'Vegetables', items: ['Broccoli', 'Carrot', 'Cucumber', 'Lettuce', 'Potato', 'Spinach', 'Tomato'] },
  { value: 'Herbs', items: ['Basil', 'Mint', 'Oregano', 'Parsley', 'Rosemary', 'Thyme'] },
]

export default function ComboboxGrouped() {
  return (
    <div className="w-full max-w-70">
      <Combobox items={groups}>
        <ComboboxInput placeholder="Search produce" aria-label="Search produce" />
        <ComboboxContent>
          <ComboboxEmpty>No produce found.</ComboboxEmpty>
          <ScrollArea className="max-h-72">
            <ComboboxList className="overflow-y-visible">
              {(group: ProduceGroup) => (
                <ComboboxGroup key={group.value} items={group.items}>
                  <ComboboxLabel>{group.value}</ComboboxLabel>
                  <ComboboxCollection>
                    {(item: string) => (
                      <ComboboxItem key={item} value={item}>
                        {item}
                      </ComboboxItem>
                    )}
                  </ComboboxCollection>
                </ComboboxGroup>
              )}
            </ComboboxList>
          </ScrollArea>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Grid layout [#grid-layout]

Set `grid` on the root to lay options out in a grid — arrow keys then move in two dimensions. `ComboboxList`'s `cols` controls how many items per row (defaults to 2 in grid mode); give each item `flex-1` to fill its cell.

```tsx
'use client'

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const tags = ['React', 'Vue', 'Svelte', 'Angular', 'Solid', 'Qwik', 'Preact', 'Lit', 'Alpine', 'Ember']

export default function ComboboxGrid() {
  return (
    <div className="w-full max-w-70">
      <Combobox items={tags} grid>
        <ComboboxInput placeholder="Search a library" aria-label="Search a library" />
        <ComboboxContent>
          <ComboboxEmpty>No libraries found.</ComboboxEmpty>
          <ComboboxList cols={2}>
            {(item: string) => (
              <ComboboxItem key={item} value={item} className="flex-1">
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Multiple selection [#multiple-selection]

Set `multiple` and swap `ComboboxInput` for `ComboboxChips`: each selected value becomes a removable `ComboboxChip`, with the text field inline for adding more. The value is an array.

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxChips,
  ComboboxChip,
  ComboboxValue,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']

export default function ComboboxMultiple() {
  const [value, setValue] = React.useState<string[]>(['Next.js', 'Astro'])

  return (
    <div className="w-full max-w-70">
      <Combobox items={frameworks} multiple value={value} onValueChange={(v) => setValue(v as string[])}>
        <ComboboxChips placeholder="Add frameworks">
          <ComboboxValue>
            {(selected: string[]) =>
              selected.map((item) => (
                <ComboboxChip key={item} aria-label={item}>
                  {item}
                </ComboboxChip>
              ))
            }
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Creatable [#creatable]

Let users add an option that isn't in the list. Control the `items`, `value`, and `inputValue` yourself: when the typed query has no exact match, append a synthetic "create" item, and in `onValueChange` detect it to add the new entry to your data and select it. Here each created label is added to a multi-select chip field.

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxChips,
  ComboboxChip,
  ComboboxValue,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'
import { Plus } from '@appica/icons-react'

interface Label {
  id: string
  value: string
  creatable?: string
}

const initialLabels: Label[] = [
  { id: 'bug', value: 'bug' },
  { id: 'documentation', value: 'documentation' },
  { id: 'enhancement', value: 'enhancement' },
  { id: 'help-wanted', value: 'help wanted' },
  { id: 'good-first-issue', value: 'good first issue' },
]

export default function ComboboxCreatable() {
  const [labels, setLabels] = React.useState<Label[]>(initialLabels)
  const [selected, setSelected] = React.useState<Label[]>([])
  const [query, setQuery] = React.useState('')

  const trimmed = query.trim()
  const lowered = trimmed.toLocaleLowerCase()
  const exactExists = labels.some((label) => label.value.trim().toLocaleLowerCase() === lowered)
  const items: Label[] =
    trimmed !== '' && !exactExists
      ? [...labels, { id: `create:${lowered}`, value: `Create "${trimmed}"`, creatable: trimmed }]
      : labels

  return (
    <div className="w-full max-w-70">
      <Combobox
        items={items}
        multiple
        value={selected}
        inputValue={query}
        onInputValueChange={setQuery}
        itemToStringLabel={(item) => (item as Label).value}
        onValueChange={(next) => {
          const list = next as Label[]
          const created = list.find((item) => item.creatable && !selected.some((s) => s.id === item.id))
          if (created?.creatable) {
            const label: Label = {
              id: created.creatable.toLocaleLowerCase().replace(/\s+/g, '-'),
              value: created.creatable,
            }
            setLabels((prev) => (prev.some((l) => l.id === label.id) ? prev : [...prev, label]))
            setSelected((prev) => [...prev, label])
            setQuery('')
            return
          }
          setSelected(list.filter((item) => !item.creatable))
          setQuery('')
        }}
      >
        <ComboboxChips placeholder="Add labels…">
          <ComboboxValue>
            {(value: Label[]) =>
              value.map((label) => (
                <ComboboxChip key={label.id} aria-label={label.value}>
                  {label.value}
                </ComboboxChip>
              ))
            }
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No labels found.</ComboboxEmpty>
          <ComboboxList>
            {(item: Label) =>
              item.creatable ? (
                <ComboboxItem key={item.id} value={item}>
                  <Plus data-icon="start" />
                  Create &ldquo;{item.creatable}&rdquo;
                </ComboboxItem>
              ) : (
                <ComboboxItem key={item.id} value={item}>
                  {item.value}
                </ComboboxItem>
              )
            }
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Controlled [#controlled]

Drive the selection yourself with `value` + `onValueChange`. The input's text is managed separately — control it too with `inputValue` + `onInputValueChange` when you need to.

```tsx
'use client'

import * as React from 'react'
import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from '@appica/ui-react/combobox'

const frameworks = ['Next.js', 'Remix', 'Astro', 'Nuxt.js', 'SvelteKit', 'SolidStart', 'Gatsby', 'Vite']

export default function ComboboxControlled() {
  const [value, setValue] = React.useState<string | null>(null)

  return (
    <div className="flex w-full max-w-70 flex-col gap-3">
      <Combobox items={frameworks} value={value} onValueChange={(v) => setValue(v as string | null)} clearable>
        <ComboboxInput placeholder="Search a framework" aria-label="Choose a framework" />
        <ComboboxContent>
          <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
      <p className="text-foreground-muted text-sm">
        Selected: <span className="text-foreground font-medium">{value ?? '—'}</span>
      </p>
    </div>
  )
}
```

## RTL [#rtl]

Every Appica UI component supports right-to-left layouts out of the box. Set the `dir` attribute on a container (commonly your `<html>` element) so CSS logical properties resolve correctly, and wrap your tree in `DirectionProvider` so direction-aware behavior — roving focus, popup placement, and the like — follows the same direction.

```tsx
import { DirectionProvider } from '@appica/ui-react/providers/direction-provider'

export default function RootLayout({ children }) {
  return (
    <html lang="ar" dir="rtl">
      <body>
        <DirectionProvider dir="rtl">{children}</DirectionProvider>
      </body>
    </html>
  )
}
```

For setup details and caveats, see the [RTL guide](/ui/docs/react/rtl).

<RtlPreview component="combobox" />

## API reference [#api-reference]

`Combobox` wraps [Base UI's Combobox](https://base-ui.com/react/components/combobox). Each part forwards every remaining prop to the matching Base UI primitive, so anything it accepts works here — the tables below list the Appica additions and the most common props. See the Base UI docs for the complete API.

### Combobox [#combobox]

The root. Holds the data and the shared styling/behavior options; renders no DOM of its own.

| Prop                 | <ColMinWidth width="180">Type</ColMinWidth>          | Default     | <ColMinWidth width="220">Description</ColMinWidth>                                   |
| -------------------- | ---------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------ |
| `items`              | <Code>readonly T\[] \| {'{ value, items }[]'}</Code> | —           | The data to filter. A flat array, or `{ value, items }` objects for grouped options. |
| `size`               | <Code>'sm' \| 'md' \| 'lg'</Code>                    | `'md'`      | Input height, popup radius, and item sizing.                                         |
| `variant`            | <Code>'outline' \| 'soft'</Code>                     | `'outline'` | Input appearance — bordered or filled.                                               |
| `clearable`          | `boolean`                                            | `false`     | Render a clear button inside the input when a value is present.                      |
| `icon`               | `boolean`                                            | `true`      | Render a chevron button that toggles the popup.                                      |
| `grid`               | `boolean`                                            | `false`     | Lay options out as a navigable grid instead of a list.                               |
| `multiple`           | `boolean`                                            | `false`     | Allow selecting several values; the value becomes an array.                          |
| `value`              | <Code>T \| T\[] \| null</Code>                       | —           | Controlled selection. Pair with `onValueChange`.                                     |
| `defaultValue`       | <Code>T \| T\[] \| null</Code>                       | —           | Uncontrolled initial selection.                                                      |
| `onValueChange`      | <Code>(value, details) => void</Code>                | —           | Fires when the selection changes.                                                    |
| `inputValue`         | `string`                                             | —           | Controlled input text. Pair with `onInputValueChange`.                               |
| `onInputValueChange` | <Code>(value, details) => void</Code>                | —           | Fires as the typed text changes.                                                     |
| `itemToStringLabel`  | <Code>(item: T) => string</Code>                     | —           | For object items: the text the input shows and the filter matches against.           |
| `itemToStringValue`  | <Code>(item: T) => string</Code>                     | —           | For object items: the string value reported for forms.                               |
| `mode`               | <Code>'list' \| 'inline' \| 'both' \| 'none'</Code>  | `'list'`    | List filtering vs. inline autocompletion behavior.                                   |
| `autoHighlight`      | <Code>boolean \| 'always'</Code>                     | `false`     | Highlight the first matching item as you type.                                       |
| `disabled`           | `boolean`                                            | `false`     | Disable the whole control.                                                           |
| `required`           | `boolean`                                            | `false`     | Require a value before the form submits.                                             |
| `name`               | `string`                                             | —           | Field name submitted with a form.                                                    |

### ComboboxInput [#comboboxinput]

The text field. Renders Base UI's `Input` inside an `InputGroup`, styled like [`Input`](/ui/components/react/input).

| Prop           | Type                                      | Default | Description                                                    |
| -------------- | ----------------------------------------- | ------- | -------------------------------------------------------------- |
| `startSlot`    | `ReactNode`                               | —       | Adornment rendered before the field, inside the input frame.   |
| `endSlot`      | `ReactNode`                               | —       | Adornment rendered after the field, before the controls.       |
| `placeholder`  | `string`                                  | `' '`   | Placeholder text.                                              |
| `disabled`     | `boolean`                                 | `false` | Disables the field and the clear/chevron buttons.              |
| `aria-invalid` | <Code>boolean \| 'true' \| 'false'</Code> | —       | Marks the error state; mirrored to `data-invalid` for styling. |
| `className`    | `string`                                  | —       | Extra classes on the input group, merged via `tailwind-merge`. |

Also forwards every native `<input>` prop (`onChange`, `name`, `aria-*`, `ref`, …).

### ComboboxTrigger [#comboboxtrigger]

A select-style trigger button (styled like [`Input`](/ui/components/react/input)) that shows the value via `ComboboxValue` and opens the popup. Use it — instead of `ComboboxInput` as the field — when the search input should live inside `ComboboxContent`.

| Prop        | Type        | Default | Description                                           |
| ----------- | ----------- | ------- | ----------------------------------------------------- |
| `startSlot` | `ReactNode` | —       | Adornment before the value, inside the trigger frame. |
| `endSlot`   | `ReactNode` | —       | Adornment after the value, before the chevron.        |
| `children`  | `ReactNode` | —       | The trigger content — typically a `ComboboxValue`.    |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`.           |

Also forwards every native `<button>` prop (`onClick`, `disabled`, `aria-*`, `ref`, …).

### ComboboxChips [#comboboxchips]

The chip-based input for `multiple` selection — render it instead of `ComboboxInput`.

| Prop          | <ColMinWidth width="160">Type</ColMinWidth> | Default | Description                                                   |
| ------------- | ------------------------------------------- | ------- | ------------------------------------------------------------- |
| `placeholder` | `string`                                    | —       | Placeholder for the inline text field.                        |
| `inputProps`  | `object`                                    | —       | Props forwarded to the inner text `input`.                    |
| `children`    | `ReactNode`                                 | —       | A `ComboboxValue` mapping selected values to `ComboboxChip`s. |
| `className`   | `string`                                    | —       | Extra classes, merged via `tailwind-merge`.                   |

### ComboboxChip [#comboboxchip]

One removable chip representing a selected value.

| Prop        | Type        | Default | Description                                   |
| ----------- | ----------- | ------- | --------------------------------------------- |
| `children`  | `ReactNode` | —       | The chip label; a remove (✕) button is added. |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`.   |

### ComboboxContent [#comboboxcontent]

The portalled, auto-positioned popup wrapping the list. Accepts the shared floating props.

| Prop              | <ColMinWidth width="160">Type</ColMinWidth>         | Default    | Description                                                     |
| ----------------- | --------------------------------------------------- | ---------- | --------------------------------------------------------------- |
| `side`            | <Code>'top' \| 'right' \| 'bottom' \| 'left'</Code> | `'bottom'` | Preferred side of the input to open on.                         |
| `align`           | <Code>'start' \| 'center' \| 'end'</Code>           | —          | Alignment along that side.                                      |
| `sideOffset`      | `number`                                            | `6`        | Gap between the input and the popup.                            |
| `alignOffset`     | `number`                                            | —          | Shift along the alignment axis.                                 |
| `container`       | <Code>HTMLElement \| ref \| null</Code>             | —          | Portal target — render the popup into a different node.         |
| `keepMounted`     | `boolean`                                           | `false`    | Keep the popup mounted in the DOM while closed.                 |
| `positionerProps` | `object`                                            | —          | Escape hatch for the positioner element (`style`, `render`, …). |
| `portalProps`     | `object`                                            | —          | Escape hatch for the portal element (`style`, `render`, …).     |
| `className`       | `string`                                            | —          | Extra classes on the popup.                                     |

### ComboboxList [#comboboxlist]

The scrollable list container.

| Prop       | <ColMinWidth width="220">Type</ColMinWidth>          | Default | Description                                                           |
| ---------- | ---------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `cols`     | `number`                                             | —       | Items per row in grid mode (defaults to 2 when `grid` is set).        |
| `children` | <Code>ReactNode \| (item, index) => ReactNode</Code> | —       | A render function over the filtered items, or static `ComboboxItem`s. |

### ComboboxItem [#comboboxitem]

One option in the list.

| Prop        | Type      | Default | Description                                             |
| ----------- | --------- | ------- | ------------------------------------------------------- |
| `value`     | `T`       | —       | The item this option represents; reported on selection. |
| `disabled`  | `boolean` | `false` | Make the option unselectable.                           |
| `className` | `string`  | —       | Extra classes (e.g. `flex-1` to fill a grid cell).      |

### ComboboxEmpty [#comboboxempty]

Shown only when the filter matches nothing — otherwise it renders nothing.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The empty-state message.                    |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### ComboboxGroup [#comboboxgroup]

Wraps one section of a grouped list. Pass the section's `items` so a nested `ComboboxCollection` can render them, and label it with a `ComboboxLabel`.

| Prop        | <ColMinWidth width="200">Type</ColMinWidth> | Default | Description                                          |
| ----------- | ------------------------------------------- | ------- | ---------------------------------------------------- |
| `items`     | `readonly T[]`                              | —       | The group's items, consumed by `ComboboxCollection`. |
| `children`  | `ReactNode`                                 | —       | A `ComboboxLabel` plus the group's items.            |
| `className` | `string`                                    | —       | Extra classes, merged via `tailwind-merge`.          |

### ComboboxLabel [#comboboxlabel]

The visible heading for a `ComboboxGroup`.

| Prop        | Type        | Default | Description                                 |
| ----------- | ----------- | ------- | ------------------------------------------- |
| `children`  | `ReactNode` | —       | The label text.                             |
| `className` | `string`    | —       | Extra classes, merged via `tailwind-merge`. |

### ComboboxCollection [#comboboxcollection]

Renders the items of the enclosing `ComboboxGroup` from a render function.

| Prop       | <ColMinWidth width="200">Type</ColMinWidth> | Default | Description                          |
| ---------- | ------------------------------------------- | ------- | ------------------------------------ |
| `children` | <Code>(item, index) => ReactNode</Code>     | —       | Render function for each group item. |

### ComboboxValue [#comboboxvalue]

Renders the current value — typically a function child mapping the selected value(s) to chips or text.

| Prop       | <ColMinWidth width="200">Type</ColMinWidth>    | Default | Description                                        |
| ---------- | ---------------------------------------------- | ------- | -------------------------------------------------- |
| `children` | <Code>ReactNode \| (value) => ReactNode</Code> | —       | Static content, or a render function of the value. |

### ComboboxSeparator [#comboboxseparator]

A thin divider between sections.

| Prop        | Type     | Default | Description                                 |
| ----------- | -------- | ------- | ------------------------------------------- |
| `className` | `string` | —       | Extra classes, merged via `tailwind-merge`. |

## Accessibility [#accessibility]

* The input has `role="combobox"`; the popup is a `listbox` (or `grid` when `grid` is set) and items are `option`/`gridcell`. Selection and active state are wired through `aria-activedescendant`.
* **↑/↓** move the highlight, **Enter** selects, **Escape** closes the popup, and typing filters the list. In `multiple` mode, **Backspace** in an empty field removes the last chip.
* The clear and chevron buttons have `aria-label`s (`Clear selection`, `Toggle popup`) and become `disabled` when the input is disabled; chip remove buttons are labelled `Remove`.
* Always give `ComboboxInput` (or `ComboboxChips`) a visible label or an `aria-label`.
* `aria-invalid` on the input is bridged to `data-invalid` so the error state styles with or without a wrapping `Field`.
* Popup enter/exit transitions are skipped when the user prefers reduced motion.
