# Combobox (/ui/components/react/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).

### Reaching the positioner [#reaching-the-positioner]

`ComboboxContent` is the popup, so its `className` styles the popup. The elements around it have their own escape hatches: `positionerProps` for the element that places it and `portalProps` for the portal. The positioning props you reach for most - `side`, `align`, `sideOffset`, `anchor`, the collision props - are already flat props on `ComboboxContent`.

The common reason to use one is theming. A popup portals to `<body>`, so CSS inheritance stops at that boundary and a popup opened from inside a scoped dark region renders light. Putting the class on the positioner fixes it, because the class only has to land on some ancestor of the popup:

```tsx
<ComboboxContent positionerProps={{ className: 'dark' }}>…</ComboboxContent>
```

The flat `container` prop moves the portal target itself. It's for shadow DOM and iframes, not theming - see [Dark Mode](/ui/docs/react/dark-mode#scoped-dark-regions-and-portals) and [Composition](/ui/docs/react/composition#portals-and-the-container-prop).

## 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 grays 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).


## 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                   | Type                            | Default     | Description                                                                                         |
| ---------------------- | ---------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `items`                | `readonly T\[] \| {'{ value, items }[]'} \| ItemCollection` | -           | The data to filter. A flat array, `{ value, items }` objects for grouped options, or a collection from Base UI's `Combobox.createItems()`. |
| `size`                 | `'sm' \| 'md' \| 'lg'`                                      | `'md'`      | Input height, popup radius, and item sizing.                                                                                               |
| `variant`              | `'outline' \| 'soft'`                                       | `'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`                | `T \| T\[] \| null`                                         | -           | Controlled selection. Pair with `onValueChange`.                                                                                           |
| `defaultValue`         | `T \| T\[] \| null`                                         | -           | Uncontrolled initial selection.                                                                                                            |
| `onValueChange`        | `(value, details) => void`                                  | -           | Fires when the selection changes.                                                                                                          |
| `inputValue`           | `string`                                                               | -           | Controlled input text. Pair with `onInputValueChange`.                                                                                     |
| `onInputValueChange`   | `(value, details) => void`                                  | -           | Fires as the typed text changes.                                                                                                           |
| `itemToStringLabel`    | `(item: T) => string`                                       | -           | For object items: the text the input shows and the filter matches against.                                                                 |
| `itemToStringValue`    | `(item: T) => string`                                       | -           | For object items: the string value reported for forms.                                                                                     |
| `mode`                 | `'list' \| 'inline' \| 'both' \| 'none'`                    | `'list'`    | List filtering vs. inline autocompletion behavior.                                                                                         |
| `autoHighlight`        | `boolean \| 'always'`                                       | `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.                                                                                                          |
| `filter`               | `function`                                                             | -           | Match items against the query. Return `true` to keep an item.                                                                              |
| `children`             | `ReactNode`                                                            | -           | The content.                                                                                                                               |
| `inline`               | `boolean`                                                              | `false`     | Render the list inline instead of in a popup. Pair with an unconditional `open`.                                                           |
| `modal`                | `boolean`                                                              | `false`     | Trap interaction inside the popup while it is open.                                                                                        |
| `open`                 | `boolean`                                                              | -           | Controlled open state. Pair with `onOpenChange`.                                                                                           |
| `defaultOpen`          | `boolean`                                                              | `false`     | Uncontrolled initial open state.                                                                                                           |
| `onOpenChangeComplete` | `function`                                                             | -           | Fires once the open/close animation finishes.                                                                                              |
| `defaultInputValue`    | `string`                                                               | -           | Uncontrolled initial input value.                                                                                                          |
| `openOnInputClick`     | `boolean`                                                              | `true`      | Open the popup when the input is clicked.                                                                                                  |
| `readOnly`             | `boolean`                                                              | `false`     | Prevent choosing a different option.                                                                                                       |
| `loopFocus`            | `boolean`                                                              | `true`      | Wrap arrow-key focus from the end of the list back to the input.                                                                           |
| `inputRef`             | `Ref`                                                                  | -           | Ref to the hidden input element.                                                                                                           |
| `filteredItems`        | `array`                                                                | -           | Pre-filtered items to render, bypassing the built-in filter.                                                                               |
| `virtualized`          | `boolean`                                                              | `false`     | Tell the list its items are virtualized externally.                                                                                        |
| `limit`                | `number`                                                               | `-1`        | Maximum items to render. `-1` for no limit.                                                                                                |
| `locale`               | `string`                                                               | -           | Locale used for string comparison. Defaults to the runtime locale.                                                                         |
| `autoComplete`         | `string`                                                               | -           | Autofill hint passed to the browser.                                                                                                       |
| `highlightItemOnHover` | `boolean`                                                              | `true`      | Highlight items as the pointer moves over them.                                                                                            |
| `isItemEqualToValue`   | `function`                                                             | -           | Compare an item to the selected value. Needed when values are objects.                                                                     |
| `actionsRef`           | `Ref`                                                                  | -           | Ref exposing imperative `unmount()` / `close()`.                                                                                           |
| `onOpenChange`         | `function`                                                             | -           | Fires when the popup opens or closes.                                                                                                      |
| `onItemHighlighted`    | `function`                                                             | -           | Fires when an item is highlighted or unhighlighted.                                                                                        |

### 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` | `boolean \| 'true' \| 'false'`                     | -       | Marks the error state; mirrored to `data-invalid` for styling.        |
| `render`       | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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.                               |
| `disabled`     | `boolean`                                                     | `false` | Disable user interaction.                                                    |
| `render`       | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component.        |
| `nativeButton` | `boolean`                                                     | `true`  | Whether `render` produces a native `<button>`. Set `false` when it does not. |
| `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          | Type                   | Default | Description                                                           |
| ------------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `placeholder` | `string`                                                      | -       | Placeholder for the inline text field.                                |
| `inputProps`  | `object`                                                      | -       | Props forwarded to the inner text `input`.                            |
| `render`      | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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                    | Type                                           | Default                | Description                                                           |
| ----------------------- | ------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------- |
| `side`                  | `'top' \| 'right' \| 'bottom' \| 'left' \| 'inline-end' \| 'inline-start'` | `'bottom'`             | Preferred side of the input to open on.                               |
| `align`                 | `'start' \| 'center' \| 'end'`                                             | -                      | Alignment along that side.                                            |
| `sideOffset`            | `number`                                                                              | `6`                    | Gap between the input and the popup.                                  |
| `alignOffset`           | `number`                                                                              | -                      | Shift along the alignment axis.                                       |
| `container`             | `HTMLElement \| ref \| null`                                               | -                      | 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`, …).           |
| `render`                | `ReactElement \| ((props, state) => ReactElement)`                         | -                      | Replace the underlying element, or compose it with another component. |
| `initialFocus`          | `boolean \| ref \| function`                                               | -                      | Where focus goes when the popup opens.                                |
| `finalFocus`            | `boolean \| ref \| function`                                               | -                      | Where focus goes when the popup closes.                               |
| `anchor`                | `Element \| ref \| VirtualElement`                                         | -                      | Element to position against, instead of the trigger.                  |
| `positionMethod`        | `'absolute' \| 'fixed'`                                                    | `'absolute'`           | CSS `position` used for the popup.                                    |
| `collisionBoundary`     | `Boundary`                                                                            | `'clipping-ancestors'` | Element or rectangle the popup is confined to.                        |
| `collisionPadding`      | `number`                                                                              | `5`                    | Extra space to keep from the boundary edge.                           |
| `collisionAvoidance`    | `CollisionAvoidance`                                                                  | -                      | How the popup reacts when it would overflow the boundary.             |
| `sticky`                | `boolean`                                                                             | `false`                | Keep the popup in view after the anchor scrolls away.                 |
| `arrowPadding`          | `number`                                                                              | `5`                    | Minimum gap between the arrow and the popup's corners.                |
| `disableAnchorTracking` | `boolean`                                                                             | `false`                | Stop the popup following layout shifts of its anchor.                 |
| `className`             | `string`                                                                              | -                      | Extra classes on the popup.                                           |

### ComboboxList [#comboboxlist]

The scrollable list container.

| Prop        | Type                   | Default | Description                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `cols`      | `number`                                                      | -       | Items per row in grid mode (defaults to 2 when `grid` is set).        |
| `className` | `string`                                                      | -       | Extra classes, merged via `tailwind-merge`.                           |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `children`  | `ReactNode \| (item, index) => ReactNode`          | -       | 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.                                             |
| `children`     | `ReactNode`                                                   | -       | The content.                                                              |
| `render`       | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component.     |
| `nativeButton` | `boolean`                                                     | `false` | Whether `render` produces a native `<button>`. Set `true` when it does.   |
| `index`        | `number`                                                      | -       | The item's index in the list. Set it to skip automatic index calculation. |
| `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                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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        | Type                   | Default | Description                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `items`     | `readonly T[]`                                                | -       | The group's items, consumed by `ComboboxCollection`.                  |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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                                                           |
| ----------- | ------------------------------------------------------------- | ------- | --------------------------------------------------------------------- |
| `render`    | `ReactElement \| ((props, state) => ReactElement)` | -       | Replace the underlying element, or compose it with another component. |
| `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       | Type | Default | Description                          |
| ---------- | ------------------------------------------- | ------- | ------------------------------------ |
| `children` | `(item, index) => ReactNode`     | -       | 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          | Type    | Default | Description                                                    |
| ------------- | ---------------------------------------------- | ------- | -------------------------------------------------------------- |
| `placeholder` | `string`                                       | -       | Text shown when nothing is selected. Overridden by `children`. |
| `children`    | `ReactNode \| (value) => ReactNode` | -       | Static content, or a render function of the value.             |

### ComboboxSeparator [#comboboxseparator]

A thin divider between sections.

| Prop          | Type                                                          | Default        | Description                                                           |
| ------------- | ------------------------------------------------------------- | -------------- | --------------------------------------------------------------------- |
| `render`      | `ReactElement \| ((props, state) => ReactElement)` | -              | Replace the underlying element, or compose it with another component. |
| `orientation` | `'horizontal' \| 'vertical'`                       | `'horizontal'` | Layout axis.                                                          |
| `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 labeled `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.
