# Combobox

> A filterable select with chips, groups and async results, in a popup that resizes as you type.

Docs: https://hextaui.com/docs/combobox
Markdown: https://hextaui.com/docs/combobox.md

```tsx title="components/examples/combobox/demo.tsx"
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxDemo() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-demo">Fruit</Label>
      <Combobox items={fruits}>
        <ComboboxInput id="combobox-demo" placeholder="Select a fruit" />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

## Installation

### CLI

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

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/combobox.tsx"
"use client"

import * as React from "react"
import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"
import { useDirection } from "@base-ui/react/direction-provider"
import {
  IconCheck,
  IconChevronDown,
  IconSearch,
  IconX,
} from "@tabler/icons-react"
import { cva } from "class-variance-authority"
import { cn } from "cn"

type ClassName<State> =
  string | ((state: State) => string | undefined) | undefined

function mergeClassName<State>(base: string, className: ClassName<State>) {
  return typeof className === "function"
    ? (state: State) => cn(base, className(state))
    : cn(base, className)
}

const ComboboxFieldContext =
  React.createContext<React.RefObject<HTMLElement | null> | null>(null)

const ComboboxContentContext = React.createContext(false)

function mergeRefs<T>(...refs: (React.Ref<T> | undefined)[]) {
  return (node: T | null) => {
    for (const ref of refs) {
      if (typeof ref === "function") {
        ref(node)
      } else if (ref) {
        ref.current = node
      }
    }
  }
}

function markReady(node: HTMLElement | null) {
  if (node) {
    requestAnimationFrame(() => {
      node.dataset.ready = ""
    })
  }
}

function useFieldRef<T extends HTMLElement>(
  ref: React.Ref<T> | undefined,
  ...extra: React.RefCallback<T>[]
) {
  const fieldRef = React.useContext(ComboboxFieldContext)
  const [first] = extra

  return React.useMemo(
    () => mergeRefs<T>(fieldRef as React.Ref<T> | null, ref, first),
    [fieldRef, ref, first]
  )
}

function Combobox<
  Value,
  Multiple extends boolean | undefined = false,
  Item = Value,
>(props: ComboboxPrimitive.Root.Props<Value, Multiple, Item>) {
  const fieldRef = React.useRef<HTMLElement | null>(null)

  return (
    <ComboboxFieldContext.Provider value={fieldRef}>
      <ComboboxPrimitive.Root {...props} />
    </ComboboxFieldContext.Provider>
  )
}

function ComboboxValue(props: ComboboxPrimitive.Value.Props) {
  return <ComboboxPrimitive.Value {...props} />
}

const comboboxFieldVariants = cva(
  "w-full min-w-0 rounded-md bg-transparent text-sm inset-ring-(length:--hairline) inset-ring-input transition-[color,box-shadow,border-color] outline-none focus-visible:outline-hidden dark:bg-input/30 forced-colors:border"
)

const comboboxIconButton =
  "relative inline-flex size-7 shrink-0 items-center justify-center rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-md)-0.3125rem))] text-muted-foreground outline-none focus-visible:outline-hidden select-none after:absolute after:-inset-1 hover:bg-muted hover:text-foreground focus-visible:ring-3 focus-visible:ring-focus-ring disabled:pointer-events-none pointer-coarse:after:-inset-2 dark:hover:bg-muted/50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"

function ComboboxTrigger({
  className,
  children,
  render,
  ref,
  ...props
}: ComboboxPrimitive.Trigger.Props) {
  const setRef = useFieldRef(ref)
  const styled = render === undefined

  return (
    <ComboboxPrimitive.Trigger
      ref={setRef}
      data-slot="combobox-trigger"
      render={render}
      className={mergeClassName(
        cn(
          "group/combobox-trigger [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
          styled &&
            cn(
              comboboxFieldVariants(),
              "inline-flex h-9 items-center justify-between gap-2 ps-2.5 pe-2 text-start select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:inset-ring-destructive data-placeholder:text-muted-foreground data-popup-open:inset-ring-ring dark:hover:bg-input/50 pointer-coarse:text-[1rem] [@media(hover:hover)]:hover:bg-muted/50"
            )
        ),
        className
      )}
      {...props}
    >
      {children !== undefined ? (
        <span
          data-slot="combobox-trigger-value"
          className="min-w-0 flex-1 truncate text-start"
        >
          {children}
        </span>
      ) : null}
      <IconChevronDown
        aria-hidden
        data-slot="combobox-trigger-icon"
        className="text-muted-foreground transition-transform duration-200 ease-spring group-data-popup-open/combobox-trigger:rotate-180 motion-reduce:transition-none"
      />
    </ComboboxPrimitive.Trigger>
  )
}

function ComboboxClear({
  className,
  children,
  ...props
}: ComboboxPrimitive.Clear.Props) {
  return (
    <ComboboxPrimitive.Clear
      data-slot="combobox-clear"
      aria-label="Clear selection"
      className={mergeClassName(
        cn(
          comboboxIconButton,
          "transition-[color,background-color,opacity,scale] duration-150 ease-out-quint data-ending-style:opacity-0 data-starting-style:opacity-0 motion-safe:data-ending-style:scale-75 motion-safe:data-starting-style:scale-75"
        ),
        className
      )}
      {...props}
    >
      {children ?? <IconX />}
    </ComboboxPrimitive.Clear>
  )
}

type ComboboxInputProps = Omit<ComboboxPrimitive.Input.Props, "className"> & {
  className?: string
  showTrigger?: boolean
  showClear?: boolean
}

function ComboboxInput({
  className,
  children,
  disabled,
  showTrigger,
  showClear = false,
  ...props
}: ComboboxInputProps) {
  const inContent = React.useContext(ComboboxContentContext)
  const setFieldRef = useFieldRef<HTMLDivElement>(undefined)
  const trigger = showTrigger ?? !inContent

  return (
    <ComboboxPrimitive.InputGroup
      ref={inContent ? undefined : setFieldRef}
      data-slot="combobox-input-group"
      className={cn(
        "group/combobox-input relative flex shrink-0 items-center",
        inContent
          ? "m-1 mb-0 h-8 rounded-[var(--combobox-item-radius)] bg-muted/60 ps-2 text-sm dark:bg-input/30"
          : cn(
              comboboxFieldVariants(),
              "h-9 focus-within:ring-3 focus-within:ring-focus-ring focus-within:inset-ring-ring has-[[aria-invalid=true]]:inset-ring-destructive has-[[aria-invalid=true]]:focus-within:ring-destructive/20 data-invalid:inset-ring-destructive dark:has-[[aria-invalid=true]]:focus-within:ring-destructive/40 data-disabled:cursor-not-allowed data-disabled:opacity-50"
            ),
        className
      )}
    >
      {inContent ? (
        <IconSearch
          aria-hidden
          className="size-4 shrink-0 text-muted-foreground"
        />
      ) : null}
      <ComboboxPrimitive.Input
        data-slot="combobox-input"
        disabled={disabled}
        className={cn(
          "h-full w-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed pointer-coarse:text-[1rem]",
          inContent ? "px-2" : "ps-2.5 pe-1"
        )}
        {...props}
      />
      {trigger || showClear ? (
        <div
          data-slot="combobox-input-actions"
          className="me-1 grid shrink-0 place-items-center *:col-start-1 *:row-start-1"
        >
          {showClear ? (
            <ComboboxClear
              disabled={disabled}
              className="peer/combobox-clear"
            />
          ) : null}
          {trigger ? (
            <ComboboxPrimitive.Trigger
              data-slot="combobox-trigger"
              aria-label="Show options"
              disabled={disabled}
              className={cn(
                comboboxIconButton,
                "group/combobox-trigger transition-[color,background-color,opacity,scale] duration-150 ease-out-quint peer-data-visible/combobox-clear:pointer-events-none peer-data-visible/combobox-clear:opacity-0 motion-safe:peer-data-visible/combobox-clear:scale-75"
              )}
            >
              <IconChevronDown
                aria-hidden
                className="transition-transform duration-200 ease-spring group-data-popup-open/combobox-trigger:rotate-180 motion-reduce:transition-none"
              />
            </ComboboxPrimitive.Trigger>
          ) : null}
        </div>
      ) : null}
      {children}
    </ComboboxPrimitive.InputGroup>
  )
}

function useDirectionAttribute(dir: string | undefined) {
  const direction = useDirection()
  const fieldRef = React.useContext(ComboboxFieldContext)

  return React.useCallback(
    (popup: HTMLElement) => {
      if (dir !== undefined) {
        return
      }
      const field = fieldRef?.current
      const fieldDirection =
        field && field.isConnected ? getComputedStyle(field).direction : null
      if (fieldDirection === "rtl" || direction === "rtl") {
        popup.setAttribute("dir", "rtl")
      }
    },
    [dir, direction, fieldRef]
  )
}

function useAnimatedHeight(applyDirection: (popup: HTMLElement) => void) {
  return React.useCallback(
    (sizer: HTMLDivElement | null) => {
      const popup = sizer?.parentElement
      if (!sizer || !popup) {
        return
      }
      applyDirection(popup)
      if (typeof ResizeObserver === "undefined") {
        return
      }
      const observer = new ResizeObserver(([entry]) => {
        const height =
          entry.borderBoxSize?.[0]?.blockSize ?? entry.contentRect.height
        popup.style.height = `${height}px`
      })
      observer.observe(sizer)
      return () => {
        observer.disconnect()
        popup.style.height = ""
      }
    },
    [applyDirection]
  )
}

type ComboboxContentProps = ComboboxPrimitive.Popup.Props &
  Pick<
    ComboboxPrimitive.Positioner.Props,
    "side" | "align" | "sideOffset" | "alignOffset" | "anchor"
  >

function ComboboxContent({
  className,
  children,
  side = "bottom",
  sideOffset = 6,
  align = "start",
  alignOffset = 0,
  anchor,
  dir,
  ...props
}: ComboboxContentProps) {
  const applyDirection = useDirectionAttribute(dir)
  const sizerRef = useAnimatedHeight(applyDirection)

  return (
    <ComboboxPrimitive.Portal>
      <ComboboxPrimitive.Positioner
        data-slot="combobox-positioner"
        side={side}
        sideOffset={sideOffset}
        align={align}
        alignOffset={alignOffset}
        anchor={anchor}
        className="isolate z-50 outline-none focus-visible:outline-hidden"
      >
        <ComboboxPrimitive.Popup
          data-slot="combobox-content"
          dir={dir}
          className={mergeClassName(
            "group/combobox-content relative w-(--anchor-width) max-w-(--available-width) origin-(--transform-origin) overflow-hidden rounded-lg bg-popover text-popover-foreground shadow-md ring-(length:--hairline) ring-foreground/10 transition-[opacity,scale,height] duration-150 ease-out-quint outline-none [--combobox-item-radius:max(calc(var(--radius-sm)*0.5),calc(var(--radius-lg)-0.25rem))] focus-visible:outline-hidden has-[>[data-slot=combobox-content-sizer]>[data-slot=combobox-input-group]]:w-[max(var(--anchor-width),15rem)] data-ending-style:opacity-0 data-ending-style:duration-100 data-starting-style:opacity-0 motion-safe:data-ending-style:scale-96 motion-safe:data-starting-style:scale-96 motion-reduce:transition-opacity forced-colors:border",
            className
          )}
          {...props}
        >
          <ComboboxPrimitive.Arrow
            data-slot="combobox-origin"
            className="pointer-events-none invisible size-0"
          />
          <div
            ref={sizerRef}
            data-slot="combobox-content-sizer"
            className="flex max-h-[min(var(--available-height),24rem)] flex-col"
          >
            <ComboboxContentContext.Provider value>
              {children}
            </ComboboxContentContext.Provider>
          </div>
        </ComboboxPrimitive.Popup>
      </ComboboxPrimitive.Positioner>
    </ComboboxPrimitive.Portal>
  )
}

function ComboboxList({ className, ...props }: ComboboxPrimitive.List.Props) {
  return (
    <ComboboxPrimitive.List
      data-slot="combobox-list"
      className={mergeClassName(
        "min-h-0 scroll-py-1 overflow-y-auto overscroll-none p-1 outline-none focus-visible:outline-hidden data-empty:p-0",
        className
      )}
      {...props}
    />
  )
}

function ComboboxItem({
  className,
  children,
  ...props
}: ComboboxPrimitive.Item.Props) {
  return (
    <ComboboxPrimitive.Item
      data-slot="combobox-item"
      className={mergeClassName(
        "group/combobox-item relative flex min-h-8 w-full cursor-default items-center gap-2 rounded-(--combobox-item-radius) py-1.5 ps-2 pe-2 text-start text-sm wrap-anywhere outline-none select-none focus-visible:outline-hidden data-highlighted:bg-accent data-highlighted:text-accent-foreground forced-colors:data-highlighted:outline-2 forced-colors:data-highlighted:-outline-offset-2 forced-colors:data-highlighted:outline-solid pointer-coarse:min-h-11 data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
        className
      )}
      {...props}
    >
      {children}
      <ComboboxPrimitive.ItemIndicator
        keepMounted
        data-slot="combobox-item-indicator"
        className="ms-auto flex size-4 shrink-0 items-center justify-center opacity-0 transition-[opacity,scale] duration-150 ease-out-quint data-[selected]:opacity-100 motion-safe:scale-50 motion-safe:data-[selected]:scale-100 motion-reduce:transition-none"
      >
        <IconCheck className="text-foreground" />
      </ComboboxPrimitive.ItemIndicator>
    </ComboboxPrimitive.Item>
  )
}

function ComboboxGroup({ className, ...props }: ComboboxPrimitive.Group.Props) {
  return (
    <ComboboxPrimitive.Group
      data-slot="combobox-group"
      className={mergeClassName("flex flex-col", className)}
      {...props}
    />
  )
}

function ComboboxLabel({
  className,
  ...props
}: ComboboxPrimitive.GroupLabel.Props) {
  return (
    <ComboboxPrimitive.GroupLabel
      data-slot="combobox-label"
      className={mergeClassName(
        "px-2 pt-2 pb-1 text-xs font-medium text-muted-foreground",
        className
      )}
      {...props}
    />
  )
}

function ComboboxCollection(props: ComboboxPrimitive.Collection.Props) {
  return <ComboboxPrimitive.Collection {...props} />
}

function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
  return (
    <ComboboxPrimitive.Empty
      data-slot="combobox-empty"
      className={mergeClassName(
        "shrink-0 text-center text-sm text-balance wrap-anywhere text-muted-foreground not-empty:px-3 not-empty:py-6",
        className
      )}
      {...props}
    />
  )
}

function ComboboxStatus({
  className,
  ...props
}: ComboboxPrimitive.Status.Props) {
  return (
    <ComboboxPrimitive.Status
      data-slot="combobox-status"
      className={mergeClassName(
        "flex shrink-0 items-center gap-2 text-sm text-muted-foreground not-empty:px-3 not-empty:py-2.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function ComboboxSeparator({
  className,
  ...props
}: ComboboxPrimitive.Separator.Props) {
  return (
    <ComboboxPrimitive.Separator
      data-slot="combobox-separator"
      className={mergeClassName(
        "-mx-1 my-1 h-px shrink-0 bg-border",
        className
      )}
      {...props}
    />
  )
}

type ComboboxChipsProps = Omit<
  ComboboxPrimitive.InputGroup.Props,
  "className"
> & {
  className?: string
}

function ComboboxChips({
  className,
  children,
  ref,
  ...props
}: ComboboxChipsProps) {
  const setRef = useFieldRef<HTMLDivElement>(ref, markReady)

  return (
    <ComboboxPrimitive.InputGroup
      ref={setRef}
      data-slot="combobox-chips"
      className={cn(
        comboboxFieldVariants(),
        "flex min-h-9 cursor-text flex-wrap items-center gap-1 p-1 focus-within:ring-3 focus-within:ring-focus-ring focus-within:inset-ring-ring has-[[aria-invalid=true]]:inset-ring-destructive has-[[aria-invalid=true]]:focus-within:ring-destructive/20 data-invalid:inset-ring-destructive dark:has-[[aria-invalid=true]]:focus-within:ring-destructive/40 data-disabled:cursor-not-allowed data-disabled:opacity-50",
        className
      )}
      {...props}
    >
      <ComboboxPrimitive.Chips className="contents">
        {children}
      </ComboboxPrimitive.Chips>
    </ComboboxPrimitive.InputGroup>
  )
}

type ComboboxChipProps = ComboboxPrimitive.Chip.Props & {
  showRemove?: boolean
}

function ComboboxChip({
  className,
  children,
  showRemove = true,
  ...props
}: ComboboxChipProps) {
  return (
    <ComboboxPrimitive.Chip
      data-slot="combobox-chip"
      className={mergeClassName(
        cn(
          "inline-flex h-6 max-w-full min-w-0 items-center gap-0.5 rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-md)-0.3125rem))] bg-secondary ps-2 text-xs font-medium text-secondary-foreground transition-[background-color,box-shadow] duration-150 outline-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:outline-hidden motion-safe:in-data-ready:animate-in motion-safe:in-data-ready:animation-duration-150 motion-safe:in-data-ready:fade-in-0 motion-safe:in-data-ready:zoom-in-90 data-disabled:opacity-50 [&:focus]:bg-accent [&:focus]:text-accent-foreground",
          showRemove ? "pe-0.5" : "pe-2"
        ),
        className
      )}
      {...props}
    >
      <span data-slot="combobox-chip-label" className="min-w-0 truncate">
        {children}
      </span>
      {showRemove ? (
        <ComboboxPrimitive.ChipRemove
          data-slot="combobox-chip-remove"
          aria-label="Remove"
          className="relative inline-flex size-5 shrink-0 items-center justify-center rounded-[max(calc(var(--radius-sm)*0.5),calc(var(--radius-md)-0.4375rem))] text-muted-foreground outline-none after:absolute after:-inset-0.5 hover:bg-foreground/10 hover:text-foreground focus-visible:outline-hidden pointer-coarse:after:-inset-2 [&_svg]:pointer-events-none [&_svg]:size-3.5"
        >
          <IconX />
        </ComboboxPrimitive.ChipRemove>
      ) : null}
    </ComboboxPrimitive.Chip>
  )
}

function ComboboxChipsInput({
  className,
  ...props
}: ComboboxPrimitive.Input.Props) {
  return (
    <ComboboxPrimitive.Input
      data-slot="combobox-chips-input"
      className={mergeClassName(
        "h-6 min-w-16 flex-1 bg-transparent px-1.5 text-sm outline-none placeholder:text-muted-foreground focus-visible:outline-hidden disabled:cursor-not-allowed pointer-coarse:text-[1rem]",
        className
      )}
      {...props}
    />
  )
}

function useComboboxAnchor() {
  return React.useRef<HTMLDivElement | null>(null)
}

const useComboboxFilter = ComboboxPrimitive.useFilter
const useComboboxFilteredItems = ComboboxPrimitive.useFilteredItems
const createComboboxItems = ComboboxPrimitive.createItems

export {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxStatus,
  ComboboxSeparator,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipsInput,
  ComboboxTrigger,
  ComboboxValue,
  ComboboxClear,
  comboboxFieldVariants,
  useComboboxAnchor,
  useComboboxFilter,
  useComboboxFilteredItems,
  createComboboxItems,
}
export type {
  ComboboxInputProps,
  ComboboxContentProps,
  ComboboxChipsProps,
  ComboboxChipProps,
}
```

Update the import paths to match your project setup.

## Usage

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

```tsx
const fruits = ["Apple", "Banana", "Cherry"]

<Combobox items={fruits}>
  <ComboboxInput placeholder="Select a fruit" />
  <ComboboxContent>
    <ComboboxEmpty>No fruit found.</ComboboxEmpty>
    <ComboboxList>
      {(item: string) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
```

Pass the options to `items` and render each one with a function inside `<ComboboxList />`. The combobox filters them as you type and only renders the matches. Objects work too: their `label` is shown in the input and their `value` is submitted.

## Composition

### Input

Type in the field to filter the list.

```text
Combobox
├── ComboboxInput
└── ComboboxContent
    ├── ComboboxEmpty
    ├── ComboboxStatus
    └── ComboboxList
        ├── ComboboxItem
        ├── ComboboxGroup
        │   ├── ComboboxLabel
        │   └── ComboboxCollection
        │       └── ComboboxItem
        └── ComboboxSeparator
```

### Button trigger

A button shows the value and the search field moves into the popup.

```text
Combobox
├── ComboboxTrigger
│   └── ComboboxValue
└── ComboboxContent
    ├── ComboboxInput
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem
```

### Chips

With `multiple`, each selected item becomes a chip before the input.

```text
Combobox
├── ComboboxChips
│   └── ComboboxValue
│       ├── ComboboxChip
│       └── ComboboxChipsInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem
```

## Examples

### Clear button

`showClear` adds a clear button that takes the chevron’s place while there is a value, so the field never grows.

```tsx title="components/examples/combobox/clear.tsx"
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxWithClear() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-clear">Fruit</Label>
      <Combobox items={fruits} defaultValue="Mango">
        <ComboboxInput
          id="combobox-clear"
          placeholder="Select a fruit"
          showClear
        />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### With icons

Icons inside an item are sized and muted for you. `autoHighlight` highlights the first match while typing, so `Enter` picks it.

```tsx title="components/examples/combobox/icons.tsx"
"use client"

import type * as React from "react"
import {
  IconBrandAngular,
  IconBrandNextjs,
  IconBrandReact,
  IconBrandSvelte,
  IconBrandVue,
} from "@tabler/icons-react"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Framework = {
  value: string
  label: string
  icon: React.ComponentType<{ className?: string }>
}

const frameworks: Framework[] = [
  { value: "next", label: "Next.js", icon: IconBrandNextjs },
  { value: "react", label: "React", icon: IconBrandReact },
  { value: "vue", label: "Vue", icon: IconBrandVue },
  { value: "svelte", label: "Svelte", icon: IconBrandSvelte },
  { value: "angular", label: "Angular", icon: IconBrandAngular },
]

export function ComboboxWithIcons() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-icons">Framework</Label>
      <Combobox items={frameworks} autoHighlight>
        <ComboboxInput id="combobox-icons" placeholder="Select a framework" />
        <ComboboxContent>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          <ComboboxList>
            {(item: Framework) => (
              <ComboboxItem key={item.value} value={item}>
                <item.icon />
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Groups and separators

Pass groups shaped like `{ value, items }` and render each with `<ComboboxGroup />`, `<ComboboxLabel />` and `<ComboboxCollection />`. Empty groups hide while filtering.

```tsx title="components/examples/combobox/groups.tsx"
"use client"

import * as React from "react"

import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
  ComboboxSeparator,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Timezone = { value: string; label: string }
type TimezoneGroup = { value: string; items: Timezone[] }

const timezones: TimezoneGroup[] = [
  {
    value: "Americas",
    items: [
      { value: "America/New_York", label: "New York (GMT-4)" },
      { value: "America/Chicago", label: "Chicago (GMT-5)" },
      { value: "America/Los_Angeles", label: "Los Angeles (GMT-7)" },
      { value: "America/Sao_Paulo", label: "São Paulo (GMT-3)" },
    ],
  },
  {
    value: "Europe",
    items: [
      { value: "Europe/London", label: "London (GMT+1)" },
      { value: "Europe/Paris", label: "Paris (GMT+2)" },
      { value: "Europe/Berlin", label: "Berlin (GMT+2)" },
    ],
  },
  {
    value: "Asia",
    items: [
      { value: "Asia/Kolkata", label: "Kolkata (GMT+5:30)" },
      { value: "Asia/Tokyo", label: "Tokyo (GMT+9)" },
      { value: "Asia/Singapore", label: "Singapore (GMT+8)" },
    ],
  },
]

export function ComboboxGroups() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-groups">Timezone</Label>
      <Combobox items={timezones} autoHighlight>
        <ComboboxInput id="combobox-groups" placeholder="Select a timezone" />
        <ComboboxContent>
          <ComboboxEmpty>No timezone found.</ComboboxEmpty>
          <ComboboxList>
            {(group: TimezoneGroup, index: number) => (
              <React.Fragment key={group.value}>
                {index > 0 ? <ComboboxSeparator /> : null}
                <ComboboxGroup items={group.items}>
                  <ComboboxLabel>{group.value}</ComboboxLabel>
                  <ComboboxCollection>
                    {(item: Timezone) => (
                      <ComboboxItem key={item.value} value={item}>
                        {item.label}
                      </ComboboxItem>
                    )}
                  </ComboboxCollection>
                </ComboboxGroup>
              </React.Fragment>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Multiple

With `multiple`, selections become chips inside `<ComboboxChips />`. The popup stays open while you pick, Backspace in the empty input removes the last chip, and the arrow keys move between chips.

```tsx title="components/examples/combobox/multiple.tsx"
"use client"

import type * as React from "react"
import {
  IconBrandAngular,
  IconBrandNextjs,
  IconBrandReact,
  IconBrandSvelte,
  IconBrandVue,
} from "@tabler/icons-react"

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

type Framework = {
  value: string
  label: string
  icon: React.ComponentType<{ className?: string }>
}

const frameworks: Framework[] = [
  { value: "next", label: "Next.js", icon: IconBrandNextjs },
  { value: "react", label: "React", icon: IconBrandReact },
  { value: "vue", label: "Vue", icon: IconBrandVue },
  { value: "svelte", label: "Svelte", icon: IconBrandSvelte },
  { value: "angular", label: "Angular", icon: IconBrandAngular },
]

export function ComboboxMultiple() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-multiple">Frameworks</Label>
      <Combobox
        items={frameworks}
        multiple
        autoHighlight
        defaultValue={[frameworks[0], frameworks[1]]}
      >
        <ComboboxChips>
          <ComboboxValue>
            {(values: Framework[]) => (
              <>
                {values.map((value) => (
                  <ComboboxChip key={value.value}>{value.label}</ComboboxChip>
                ))}
                <ComboboxChipsInput
                  id="combobox-multiple"
                  placeholder={values.length > 0 ? "" : "Add frameworks"}
                />
              </>
            )}
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxContent>
          <ComboboxEmpty>No framework found.</ComboboxEmpty>
          <ComboboxList>
            {(item: Framework) => (
              <ComboboxItem key={item.value} value={item}>
                <item.icon />
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Search inside the popup

Use `<ComboboxTrigger />` for a select-like field. Put the input inside `<ComboboxContent />` and it becomes a search box with an icon, and the popup widens to at least 15rem.

```tsx title="components/examples/combobox/popup-search.tsx"
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from "@/components/ui/combobox"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxPopupSearch() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Combobox items={countries}>
        <ComboboxTrigger aria-label="Country">
          <ComboboxValue placeholder="Select a country" />
        </ComboboxTrigger>
        <ComboboxContent>
          <ComboboxInput placeholder="Search countries" />
          <ComboboxEmpty>No country found.</ComboboxEmpty>
          <ComboboxList>
            {(item: (typeof countries)[number]) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Trigger rendered as a Button

Pass `render` to the trigger to use any button. The popup anchors to it and keeps at least its width.

```tsx title="components/examples/combobox/button-trigger.tsx"
"use client"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from "@/components/ui/combobox"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxButtonTrigger() {
  return (
    <Combobox items={countries} defaultValue={countries[6]}>
      <ComboboxTrigger
        aria-label="Country"
        render={<Button variant="outline" />}
      >
        <ComboboxValue />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput placeholder="Search countries" />
        <ComboboxEmpty>No country found.</ComboboxEmpty>
        <ComboboxList>
          {(item: (typeof countries)[number]) => (
            <ComboboxItem key={item.value} value={item}>
              {item.label}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

### Controlled

Control the selection with `value` and `onValueChange`, and the popup with `open` and `onOpenChange`. Clearing sets the value to `null`.

```tsx title="components/examples/combobox/controlled.tsx"
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxControlled() {
  const [value, setValue] = React.useState<string | null>("Peach")
  const [open, setOpen] = React.useState(false)

  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-controlled">Fruit</Label>
      <Combobox
        items={fruits}
        value={value}
        onValueChange={setValue}
        open={open}
        onOpenChange={setOpen}
      >
        <ComboboxInput
          id="combobox-controlled"
          placeholder="Select a fruit"
          showClear
        />
        <ComboboxContent>
          <ComboboxEmpty>No fruit found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
      <div className="flex items-center gap-2">
        <Button size="sm" variant="outline" onClick={() => setValue("Kiwi")}>
          Pick Kiwi
        </Button>
        <Button size="sm" variant="outline" onClick={() => setOpen(!open)}>
          {open ? "Close" : "Open"}
        </Button>
      </div>
      <p className="text-sm text-muted-foreground">Value: {value ?? "none"}</p>
    </div>
  )
}
```

### Disabled, invalid and disabled items

`disabled` on the root dims the field and its buttons. `aria-invalid` on the input draws the error ring. Disabled items are skipped by the arrow keys.

```tsx title="components/examples/combobox/states.tsx"
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const fruits = [
  "Apple",
  "Apricot",
  "Banana",
  "Blackberry",
  "Blueberry",
  "Cherry",
  "Grape",
  "Grapefruit",
  "Kiwi",
  "Lychee",
  "Mango",
  "Orange",
  "Papaya",
  "Peach",
  "Pear",
  "Pineapple",
  "Plum",
  "Raspberry",
  "Strawberry",
  "Watermelon",
]

export function ComboboxStates() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-disabled">Disabled</Label>
        <Combobox items={fruits} disabled defaultValue="Apple">
          <ComboboxInput id="combobox-disabled" showClear />
          <ComboboxContent>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-invalid">Invalid</Label>
        <Combobox items={fruits}>
          <ComboboxInput
            id="combobox-invalid"
            aria-invalid
            placeholder="Required"
          />
          <ComboboxContent>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem
                  key={item}
                  value={item}
                  disabled={item.startsWith("B")}
                >
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
        <p className="text-sm text-muted-foreground">
          Items starting with B are disabled.
        </p>
      </div>
    </div>
  )
}
```

### Long content and large lists

Long and unbroken labels wrap instead of widening the popup. `limit` caps how many matches render, which keeps a 500 item list fast.

```tsx title="components/examples/combobox/long-content.tsx"
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const longItems = [
  "A very long option label that wraps onto a second line instead of pushing the popup wider than its input",
  "supercalifragilisticexpialidocious-unbroken-string-without-any-spaces-at-all-anywhere",
  "olivia.martin+newsletter-subscriptions@a-very-long-company-domain.example.com",
  "👩‍👩‍👧‍👦 Family 🧑🏽‍💻 Developer 🏳️‍🌈",
  "東京都千代田区丸の内一丁目",
  "",
  "Short",
]

const manyItems = Array.from({ length: 500 }, (_, index) => `Item ${index + 1}`)

export function ComboboxLongContent() {
  return (
    <div className="grid w-full max-w-xl gap-6 sm:grid-cols-2">
      <div className="flex w-full max-w-60 flex-col gap-2">
        <Label htmlFor="combobox-long">Long labels</Label>
        <Combobox items={longItems} defaultValue={longItems[1]}>
          <ComboboxInput id="combobox-long" placeholder="Pick one" showClear />
          <ComboboxContent>
            <ComboboxEmpty>
              Nothing matches this unusually long query, try something else.
            </ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item || "(empty)"}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="combobox-many">500 items</Label>
        <Combobox items={manyItems} limit={100}>
          <ComboboxInput id="combobox-many" placeholder="Search items" />
          <ComboboxContent>
            <ComboboxEmpty>No item found.</ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxContent>
        </Combobox>
        <p className="text-sm text-muted-foreground">
          Shows the first 100 matches.
        </p>
      </div>
    </div>
  )
}
```

### Async search

Turn off built-in filtering with `filter={null}`, fetch on `onInputValueChange`, and show progress in `<ComboboxStatus />`, which announces it to screen readers. The popup height animates as results change.

```tsx title="components/examples/combobox/async.tsx"
"use client"

import * as React from "react"
import { IconLoader2 } from "@tabler/icons-react"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxStatus,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const countries = [
  { value: "ar", label: "Argentina" },
  { value: "au", label: "Australia" },
  { value: "br", label: "Brazil" },
  { value: "ca", label: "Canada" },
  { value: "de", label: "Germany" },
  { value: "fr", label: "France" },
  { value: "in", label: "India" },
  { value: "jp", label: "Japan" },
  { value: "mx", label: "Mexico" },
  { value: "nl", label: "Netherlands" },
  { value: "es", label: "Spain" },
  { value: "se", label: "Sweden" },
  { value: "ch", label: "Switzerland" },
  { value: "za", label: "South Africa" },
  { value: "kr", label: "South Korea" },
  { value: "uk", label: "United Kingdom" },
  { value: "us", label: "United States" },
]

export function ComboboxAsync() {
  const [query, setQuery] = React.useState("")
  const [results, setResults] = React.useState(countries.slice(0, 5))
  const [loading, setLoading] = React.useState(false)
  const runRef = React.useRef(0)

  React.useEffect(() => {
    const run = ++runRef.current
    const timer = setTimeout(() => {
      setLoading(true)
      setTimeout(() => {
        if (run !== runRef.current) {
          return
        }
        const needle = query.trim().toLowerCase()
        setResults(
          countries.filter((country) =>
            country.label.toLowerCase().includes(needle)
          )
        )
        setLoading(false)
      }, 600)
    }, 150)
    return () => {
      clearTimeout(timer)
      runRef.current += 1
    }
  }, [query])

  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="combobox-async">Country</Label>
      <Combobox
        items={results}
        filter={null}
        inputValue={query}
        onInputValueChange={setQuery}
      >
        <ComboboxInput id="combobox-async" placeholder="Search countries" />
        <ComboboxContent>
          <ComboboxStatus>
            {loading ? (
              <>
                <IconLoader2 className="animate-spin" />
                Searching…
              </>
            ) : null}
          </ComboboxStatus>
          {loading ? null : (
            <ComboboxEmpty>No country matches “{query}”.</ComboboxEmpty>
          )}
          <ComboboxList>
            {(item: (typeof countries)[number]) => (
              <ComboboxItem key={item.value} value={item}>
                {item.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </div>
  )
}
```

### Inside a sheet

The popup layers above the sheet, and Escape closes the popup before the sheet.

```tsx title="components/examples/combobox/sheet.tsx"
"use client"

import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"
import {
  Sheet,
  SheetBody,
  SheetContent,
  SheetDescription,
  SheetHeader,
  SheetTitle,
  SheetTrigger,
} from "@/components/ui/sheet"

type Timezone = { value: string; label: string }
type TimezoneGroup = { value: string; items: Timezone[] }

const timezones: TimezoneGroup[] = [
  {
    value: "Americas",
    items: [
      { value: "America/New_York", label: "New York (GMT-4)" },
      { value: "America/Chicago", label: "Chicago (GMT-5)" },
      { value: "America/Los_Angeles", label: "Los Angeles (GMT-7)" },
      { value: "America/Sao_Paulo", label: "São Paulo (GMT-3)" },
    ],
  },
  {
    value: "Europe",
    items: [
      { value: "Europe/London", label: "London (GMT+1)" },
      { value: "Europe/Paris", label: "Paris (GMT+2)" },
      { value: "Europe/Berlin", label: "Berlin (GMT+2)" },
    ],
  },
  {
    value: "Asia",
    items: [
      { value: "Asia/Kolkata", label: "Kolkata (GMT+5:30)" },
      { value: "Asia/Tokyo", label: "Tokyo (GMT+9)" },
      { value: "Asia/Singapore", label: "Singapore (GMT+8)" },
    ],
  },
]

export function ComboboxInSheet() {
  return (
    <Sheet>
      <SheetTrigger render={<Button variant="outline" />}>
        Open sheet
      </SheetTrigger>
      <SheetContent>
        <SheetHeader>
          <SheetTitle>Preferences</SheetTitle>
          <SheetDescription>
            Choose the timezone for your reports.
          </SheetDescription>
        </SheetHeader>
        <SheetBody>
          <div className="flex flex-col gap-2">
            <Label htmlFor="combobox-sheet">Timezone</Label>
            <Combobox items={timezones} autoHighlight>
              <ComboboxInput
                id="combobox-sheet"
                placeholder="Select a timezone"
              />
              <ComboboxContent>
                <ComboboxEmpty>No timezone found.</ComboboxEmpty>
                <ComboboxList>
                  {(group: TimezoneGroup) => (
                    <ComboboxGroup key={group.value} items={group.items}>
                      <ComboboxLabel>{group.value}</ComboboxLabel>
                      <ComboboxCollection>
                        {(item: Timezone) => (
                          <ComboboxItem key={item.value} value={item}>
                            {item.label}
                          </ComboboxItem>
                        )}
                      </ComboboxCollection>
                    </ComboboxGroup>
                  )}
                </ComboboxList>
              </ComboboxContent>
            </Combobox>
          </div>
        </SheetBody>
      </SheetContent>
    </Sheet>
  )
}
```

### Right to left

The popup picks up the field’s direction, so the clear button, chips and items mirror without extra props.

```tsx title="components/examples/combobox/rtl.tsx"
"use client"

import { DirectionProvider } from "@base-ui/react/direction-provider"

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
} from "@/components/ui/combobox"
import { Label } from "@/components/ui/label"

const cities = ["القاهرة", "الرياض", "دبي", "بيروت", "عمّان", "الدوحة"]

export function ComboboxRtl() {
  return (
    <DirectionProvider direction="rtl">
      <div dir="rtl" className="flex w-full max-w-xs flex-col gap-4">
        <div className="flex flex-col gap-2">
          <Label htmlFor="combobox-rtl">المدينة</Label>
          <Combobox items={cities} defaultValue={cities[2]}>
            <ComboboxInput
              id="combobox-rtl"
              placeholder="اختر مدينة"
              showClear
            />
            <ComboboxContent>
              <ComboboxEmpty>لا توجد نتائج.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxContent>
          </Combobox>
        </div>
        <div className="flex flex-col gap-2">
          <Label htmlFor="combobox-rtl-chips">المدن</Label>
          <Combobox items={cities} multiple defaultValue={[cities[0]]}>
            <ComboboxChips>
              <ComboboxValue>
                {(values: string[]) => (
                  <>
                    {values.map((value) => (
                      <ComboboxChip key={value}>{value}</ComboboxChip>
                    ))}
                    <ComboboxChipsInput id="combobox-rtl-chips" />
                  </>
                )}
              </ComboboxValue>
            </ComboboxChips>
            <ComboboxContent>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxContent>
          </Combobox>
        </div>
      </div>
    </DirectionProvider>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `↓` `↑` | Opens the popup and moves the highlight through the matches. Disabled items are skipped. |
| `Enter` | Picks the highlighted item. With nothing highlighted it closes the popup and lets the form submit. |
| `Escape` | Closes the popup. When it is already closed, clears the value and the input. |
| `Home` `End` | Moves the text cursor to the start or end of the input. |
| `Backspace` | In an empty chips input, removes the last chip. On a focused chip, removes it. |
| `←` `→` | With chips, moves focus between chips and back to the input. Mirrored in right-to-left layouts. |
| `Tab` | Closes the popup and moves focus on. |

## Accessibility

- Give the input a visible `<label>` through `id` and `htmlFor`, or an `aria-label`. A `<ComboboxTrigger />` without visible text needs an `aria-label` too.
- The chevron button is labelled “Show options”, the clear button “Clear selection” and each chip’s remove button “Remove”.
- Highlighting moves with `aria-activedescendant`, so focus stays in the input while you browse.
- Inputs use a 16px font on touch screens so iOS doesn’t zoom in, and items grow to a 44px tap target.

## API reference

Built on the Base UI combobox. Every part accepts the props of the primitive it wraps; the tables list the ones you’ll use most.

### Combobox

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `Item[] \| Group[]` | – | The options. Filtered as you type and passed to the list’s render function. |
| `multiple` | `boolean` | `false` | Select several values, shown as chips. |
| `value` | `Value \| Value[] \| null` | – |  |
| `defaultValue` | `Value \| Value[] \| null` | – |  |
| `onValueChange` | `(value, details) => void` | – |  |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `false` |  |
| `onOpenChange` | `(open: boolean, details) => void` | – |  |
| `inputValue` | `string` | – |  |
| `defaultInputValue` | `string` | – |  |
| `onInputValueChange` | `(inputValue: string, details) => void` | – |  |
| `filter` | `((item, query, itemToString) => boolean) \| null` | – | Custom matching. null turns filtering off for server-side search. |
| `limit` | `number` | `-1` | Maximum number of matches to render. -1 means all. |
| `autoHighlight` | `boolean` | `false` | Highlight the first match while typing. |
| `highlightItemOnHover` | `boolean` | `true` |  |
| `openOnInputClick` | `boolean` | `true` |  |
| `loopFocus` | `boolean` | `true` | Wrap the highlight from the last item to the first. |
| `itemToStringLabel` | `(item) => string` | – | Text shown in the input for an object item. |
| `itemToStringValue` | `(item) => string` | – | Value submitted with the form for an object item. |
| `isItemEqualToValue` | `(item, value) => boolean` | – |  |
| `name` | `string` | – |  |
| `required` | `boolean` | `false` |  |
| `disabled` | `boolean` | `false` |  |
| `readOnly` | `boolean` | `false` |  |
| `modal` | `boolean` | `false` | Lock page scroll and outside clicks while open. |
| `virtualized` | `boolean` | `false` | Set when rendering items with a virtualizer. |
| `locale` | `Intl.LocalesArgument` | – | Locale used for matching. |

### ComboboxInput

Outside the popup it renders the full field. Inside `<ComboboxContent />` it becomes a compact search box.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showTrigger` | `boolean` | `true outside the popup` | Show the chevron button. Always off inside the popup unless set. |
| `showClear` | `boolean` | `false` | Show a clear button in the chevron’s place while there is a value. |
| `className` | `string` | – | Applied to the input group around the input. |
| `disabled` | `boolean` | `false` |  |
| `placeholder` | `string` | – |  |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-input-group"` | The field around the input. |
| `data-slot="combobox-input"` | The text input. |
| `data-slot="combobox-input-actions"` | Holds the chevron and clear buttons in one stacked cell. |
| `data-popup-open` | Present on the input while the popup is open. |
| `data-popup-side` | The side the popup opened on. |
| `data-list-empty` | Present when nothing matches. |
| `data-disabled` | Present when disabled. |
| `data-invalid` | Present when invalid inside a Base UI Field. |

### ComboboxTrigger

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | – | Usually a <ComboboxValue />. The chevron is added after it. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<button>` | When set, the built-in field styles are skipped. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-trigger"` | The trigger button. |
| `data-slot="combobox-trigger-value"` | Wraps the truncated value. |
| `data-slot="combobox-trigger-icon"` | The chevron. Flips while open. |
| `data-popup-open` | Present while the popup is open. |
| `data-placeholder` | Present while no value is selected. |

### ComboboxValue

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| (value) => ReactNode` | – | Render the selected value yourself, for example as chips. |
| `placeholder` | `ReactNode` | – | Shown while nothing is selected. |

### ComboboxContent

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"` |  |
| `align` | `"start" \| "center" \| "end"` | `"start"` |  |
| `sideOffset` | `number` | `6` |  |
| `alignOffset` | `number` | `0` |  |
| `anchor` | `Element \| RefObject<Element \| null> \| VirtualElement \| (() => Element \| VirtualElement \| null) \| null` | – | Position against another element. Defaults to the field. See useComboboxAnchor. |
| `dir` | `"ltr" \| "rtl"` | – | Defaults to the field’s direction. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-positioner"` | Positions the popup. |
| `data-slot="combobox-content"` | The popup surface. |
| `data-slot="combobox-content-sizer"` | Measured to animate the popup’s height as matches change. |
| `data-open` | Present while open. |
| `data-side` | The side it opened on. |
| `data-align` | Its alignment. |
| `data-empty` | Present when nothing matches. |
| `data-starting-style` | Present while animating in. |
| `data-ending-style` | Present while animating out. |
| `--combobox-item-radius` | Item radius, derived from the popup radius minus its padding. |

### ComboboxList

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode \| (item, index) => ReactNode` | – | Called for every match of items. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-list"` | The scrolling list. |

### ComboboxItem

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` | `Item` | – | The item this row represents. |
| `disabled` | `boolean` | `false` |  |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-item"` | An option. |
| `data-slot="combobox-item-indicator"` | The check, scaled in when selected. |
| `data-highlighted` | Present while highlighted. |
| `data-selected` | Present when selected. |
| `data-disabled` | Present when disabled. |

### ComboboxGroup, ComboboxLabel and ComboboxCollection

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `items` | `Item[]` | – | On ComboboxGroup: the group’s own items. |
| `children` | `(item, index) => ReactNode` | – | On ComboboxCollection: renders each match. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-group"` | A group of items. |
| `data-slot="combobox-label"` | The group heading. |

### ComboboxEmpty, ComboboxStatus and ComboboxSeparator

`<ComboboxEmpty />` shows its children only when nothing matches. `<ComboboxStatus />` is a live region for loading and result messages. Both collapse to nothing when empty.

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-empty"` | The no-results message. |
| `data-slot="combobox-status"` | The live status message. |
| `data-slot="combobox-separator"` | A divider between groups. |

### ComboboxClear

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `children` | `ReactNode` | `<IconX />` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-clear"` | Labelled “Clear selection”. |
| `data-visible` | Present while there is something to clear. |

### ComboboxChips

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `className` | `string` | – | Applied to the field that wraps the chips. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-chips"` | The field that holds chips and input. |

### ComboboxChip

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `showRemove` | `boolean` | `true` | Show the remove button. |

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-chip"` | A selected value. |
| `data-slot="combobox-chip-label"` | Its truncated label. |
| `data-slot="combobox-chip-remove"` | Labelled “Remove”. |

### ComboboxChipsInput

The text input that sits after the chips. Accepts the same props as the Base UI input.

| Attribute | Description |
| --- | --- |
| `data-slot="combobox-chips-input"` | The chips input. |

### Hooks and helpers

- `useComboboxAnchor()` returns a ref to pass to an element and to `anchor` on the content.
- `useComboboxFilter()` returns locale-aware `contains`, `startsWith` and `endsWith` matchers for `filter`.
- `useComboboxFilteredItems()` reads the current matches, for counts or virtualized lists.
- `createComboboxItems(data, { getValue })` builds an item collection whose selection value is a primitive id, like a database key, instead of the whole object.
- `comboboxFieldVariants` exposes the field styles for building custom fields.

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
