# Combobox

An input that filters a list of options as you type.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

export default function ComboboxDemo() {
  return (
    <Combobox items={frameworks}>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx lucide-react
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and `state-styles.ts`
   from the same page. `states.ts` holds the popup's surface, the items' nested
   corner, the chips frame's field surface and focus ring, and the dimmed item. The
   list, the items and the chips read their Base UI state through
   `state-styles.ts`. Include `interop.css` for icon sizing,
   button trigger backgrounds and hiding the chevron beside the clear button.

   Paste `field-state.ts` from the same installation page.

3. Add [Input](https://carbon.style/docs/components/input), [Input Group](https://carbon.style/docs/components/input-group)
   and [Button](https://carbon.style/docs/components/button). The input renders inside an
   `InputGroup` frame, and the trigger, clear and chip-remove controls are
   buttons.

4. Copy and paste the following code into your project.

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

   import type { CSSProperties } from "react"
   import { createContext, use, useRef } from "react"
   import { Combobox as ComboboxPrimitive } from "@base-ui/react/combobox"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"
   import { Check, ChevronDown, X } from "lucide-react"

   import { Button } from "@/components/ui/button"
   import { Input } from "@/components/ui/input"
   import { InputGroup, InputGroupAddon, InputGroupButton } from "@/components/ui/input-group"
   import { stateStyles } from "@/lib/state-styles"
   import { states } from "@/theme/states"
   import {
     color,
     duration,
     ease,
     layer,
     leading,
     radius,
     shadow,
     space,
     text,
   } from "@/theme/tokens.stylex"

   const ComboboxContentContext = createContext(false)

   const styles = stylex.create({
     hidden: { opacity: 0 },
     content: {
       zIndex: layer.popup,
       width: "var(--anchor-width)",
       minWidth: "9rem",
       maxWidth: "var(--available-width)",
       maxHeight: "var(--available-height)",
       overflow: "hidden",
       "--carbon-popup-radius": radius.lg,
       borderRadius: "var(--carbon-popup-radius)",
       transformOrigin: "var(--transform-origin)",
       transitionProperty: "opacity, transform",
       transitionDuration: { default: duration.fade, "@media (prefers-reduced-motion: reduce)": "0s" },
       transitionTimingFunction: ease.standard,
     },
     list: {
       maxHeight: "min(calc(18rem - 2.25rem), calc(var(--available-height) - 2.25rem))",
       overflowY: "auto",
       overscrollBehavior: "contain",
       scrollPaddingTop: space.s1,
       scrollPaddingBottom: space.s1,
       padding: space.s1,
     },
     listEmpty: { padding: 0 },
     item: {
       position: "relative",
       display: "flex",
       width: "100%",
       alignItems: "center",
       gap: space.s1_5,
       cursor: "default",
       userSelect: "none",
       paddingBlock: space.s1,
       paddingInlineStart: space.s1_5,
       paddingInlineEnd: space.s8,
       fontSize: text.sm,
       lineHeight: leading.sm,
       outlineStyle: "none",
       "--carbon-icon-size": space.s4,
     },
     indicator: {
       pointerEvents: "none",
       position: "absolute",
       insetInlineEnd: space.s2,
       display: "flex",
       width: space.s4,
       height: space.s4,
       alignItems: "center",
       justifyContent: "center",
     },
     label: {
       paddingInline: space.s1_5,
       paddingBlock: space.s1,
       fontSize: text.xs,
       lineHeight: leading.xs,
       fontWeight: 500,
       color: color.mutedForeground,
     },
     empty: {
       display: "flex",
       width: "100%",
       justifyContent: "center",
       paddingBlock: { default: space.s2, ":empty": 0 },
       textAlign: "center",
       fontSize: text.sm,
       lineHeight: leading.sm,
       color: color.mutedForeground,
     },
     separator: {
       marginInline: `calc(-1 * ${space.s1})`,
       marginBlock: space.s1,
       height: 1,
       backgroundColor: color.border,
     },
     chevron: { color: color.mutedForeground },
     frame: { width: "auto" },
     frameInPopup: {
       margin: space.s1,
       marginBlockEnd: 0,
       borderRadius: `max(0px, calc(var(--carbon-popup-radius, ${radius.lg}) - ${space.s1}))`,
       backgroundColor: `color-mix(in oklab, ${color.input} 30%, transparent)`,
     },
     inputTrigger: {
       color: color.mutedForeground,
       "--carbon-menu-open-bg": color.transparent,
       "--carbon-menu-open-fg": color.current,
     },
     chips: {
       display: "flex",
       minHeight: space.s8,
       flexWrap: "wrap",
       alignItems: "center",
       gap: space.s1,
       borderRadius: radius.lg,
       borderWidth: 1,
       borderStyle: "solid",
       paddingInline: space.s1,
       paddingBlock: space.s1,
       fontSize: text.sm,
       lineHeight: leading.sm,
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
     },
     chipsDrop: {
       borderColor: {
         default: "var(--carbon-border-color)",
         ":hover": color.fieldHoverEdge,
         ":focus-within": color.fieldFocusEdge,
       },
       boxShadow: { default: shadow.field, ":focus-within": `${shadow.field}, ${shadow.fieldFocus}` },
     },
     chip: {
       display: "flex",
       width: "fit-content",
       height: space.s5,
       alignItems: "center",
       justifyContent: "center",
       gap: space.s1,
       whiteSpace: "nowrap",
       borderRadius: radius.sm,
       paddingInline: space.s1_5,
       backgroundColor: color.muted,
       color: color.foreground,
       fontSize: text.xs,
       lineHeight: leading.xs,
       fontWeight: 500,
     },
     chipWithRemove: { paddingInlineEnd: 0 },
     chipRemove: {
       width: space.s5,
       height: space.s5,
       marginInlineStart: `calc(-1 * ${space.s1})`,
       opacity: { default: 0.5, ":hover": 1 },
     },
     chipsInput: {
       flexGrow: 1,
       minWidth: space.s16,
       height: space.s5,
       paddingInline: space.s1_5,
       borderWidth: 0,
       backgroundColor: color.transparent,
       color: color.foreground,
       fontSize: text.sm,
       lineHeight: leading.sm,
       outlineStyle: "none",
       "::placeholder": { color: color.mutedForeground },
     },
   })

   const comboboxStyles = {
     content: styles.content,
     list: styles.list,
     item: [styles.item, states.popupItem],
     itemHighlighted: states.itemHighlight,
     itemDisabled: states.disabled,
     label: styles.label,
     empty: styles.empty,
     separator: styles.separator,
     chips: [styles.chips, states.focusWithinRing, states.fieldSurface, styles.chipsDrop],
     chip: styles.chip,
     chipsInput: styles.chipsInput,
   }

   const slide = stylex.create({
     top: { transform: `scale(0.95) translateY(${space.s2})` },
     bottom: { transform: `scale(0.95) translateY(calc(-1 * ${space.s2}))` },
     left: { transform: `scale(0.95) translateX(${space.s2})` },
     right: { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
     "inline-start": { transform: `scale(0.95) translateX(${space.s2})` },
     "inline-end": { transform: `scale(0.95) translateX(calc(-1 * ${space.s2}))` },
   })

   type Overrides = { xstyle?: StyleXStyles; className?: string }

   type StateOverrides = Overrides & { style?: CSSProperties }

   function Combobox<Value, Multiple extends boolean | undefined = false>(
     props: ComboboxPrimitive.Root.Props<Value, Multiple>,
   ) {
     return <ComboboxPrimitive.Root {...props} />
   }

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

   type ComboboxTriggerProps = Omit<ComboboxPrimitive.Trigger.Props, "className"> & Overrides

   function ComboboxTrigger({ xstyle, className, children, ...props }: ComboboxTriggerProps) {
     const sx = stylex.props(xstyle)
     return (
       <ComboboxPrimitive.Trigger
         data-slot="combobox-trigger"
         {...sx}
         {...props}
         className={clsx("carbon-icons", sx.className, className)}
       >
         {children}
         <ChevronDown {...stylex.props(styles.chevron)} />
       </ComboboxPrimitive.Trigger>
     )
   }

   type ComboboxClearProps = Omit<ComboboxPrimitive.Clear.Props, "className"> & Overrides

   function ComboboxClear({ xstyle, className, ...props }: ComboboxClearProps) {
     return (
       <ComboboxPrimitive.Clear
         data-slot="combobox-clear"
         render={
           <InputGroupButton
             data-slot="combobox-clear"
             size="icon-xs"
             xstyle={xstyle}
             className={className}
           />
         }
         aria-label="Clear"
         {...props}
       >
         <X />
       </ComboboxPrimitive.Clear>
     )
   }

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

   function ComboboxInput({
     xstyle,
     className,
     children,
     disabled = false,
     "aria-invalid": ariaInvalid,
     showTrigger = true,
     showClear = false,
     ...props
   }: ComboboxInputProps) {
     const inPopup = use(ComboboxContentContext)
     const invalid = ariaInvalid != null && ariaInvalid !== false

     return (
       <ComboboxPrimitive.InputGroup
         render={
           <InputGroup
             disabled={disabled}
             invalid={invalid}
             xstyle={[styles.frame, inPopup && styles.frameInPopup, xstyle]}
             className={className}
           />
         }
       >
         <ComboboxPrimitive.Input render={<Input />} aria-invalid={invalid || undefined} {...props} />
         {(showTrigger || showClear) && (
           <InputGroupAddon align="inline-end">
             {showClear && <ComboboxClear disabled={disabled} />}
             {showTrigger && (
               <ComboboxTrigger
                 render={
                   <InputGroupButton
                     data-slot="combobox-trigger"
                     size="icon-xs"
                     xstyle={styles.inputTrigger}
                   />
                 }
                 aria-label="Open"
                 disabled={disabled}
               />
             )}
           </InputGroupAddon>
         )}
         {children}
       </ComboboxPrimitive.InputGroup>
     )
   }

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

   function ComboboxContent({
     xstyle,
     style,
     className,
     side = "bottom",
     sideOffset = 6,
     align = "start",
     alignOffset = 0,
     anchor,
     ...props
   }: ComboboxContentProps) {
     return (
       <ComboboxPrimitive.Portal>
         <ComboboxPrimitive.Positioner
           align={align}
           alignOffset={alignOffset}
           side={side}
           sideOffset={sideOffset}
           anchor={anchor}
           {...stylex.props(states.positioner)}
         >
           <ComboboxContentContext value>
             <ComboboxPrimitive.Popup
               data-slot="combobox-content"
               {...props}
               {...stateStyles(
                 (state) => [
                   states.popupSurface,
                   styles.content,
                   (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                     styles.hidden,
                   (state.transitionStatus === "starting" || state.transitionStatus === "ending") &&
                     slide[state.side],
                   xstyle,
                 ],
                 { className, style },
               )}
             />
           </ComboboxContentContext>
         </ComboboxPrimitive.Positioner>
       </ComboboxPrimitive.Portal>
     )
   }

   type ComboboxListProps = Omit<ComboboxPrimitive.List.Props, "className" | "style"> & StateOverrides

   function ComboboxList({ xstyle, style, className, ...props }: ComboboxListProps) {
     return (
       <ComboboxPrimitive.List
         data-slot="combobox-list"
         {...props}
         {...stateStyles((state) => [styles.list, state.empty && styles.listEmpty, xstyle], {
           className,
           style,
           hook: "no-scrollbar",
         })}
       />
     )
   }

   type ComboboxItemProps = Omit<ComboboxPrimitive.Item.Props, "className" | "style"> & StateOverrides

   function ComboboxItem({ xstyle, style, className, children, ...props }: ComboboxItemProps) {
     return (
       <ComboboxPrimitive.Item
         data-slot="combobox-item"
         {...props}
         {...stateStyles(
           (state) => [
             styles.item,
             states.popupItem,
             state.highlighted && states.itemHighlight,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style, hook: "carbon-icons" },
         )}
       >
         {children}
         <ComboboxPrimitive.ItemIndicator
           render={<span data-slot="combobox-item-indicator" {...stylex.props(styles.indicator)} />}
         >
           <Check />
         </ComboboxPrimitive.ItemIndicator>
       </ComboboxPrimitive.Item>
     )
   }

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

   type ComboboxLabelProps = Omit<ComboboxPrimitive.GroupLabel.Props, "className"> & Overrides

   function ComboboxLabel({ xstyle, className, ...props }: ComboboxLabelProps) {
     const sx = stylex.props(styles.label, xstyle)
     return (
       <ComboboxPrimitive.GroupLabel
         data-slot="combobox-label"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

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

   type ComboboxEmptyProps = Omit<ComboboxPrimitive.Empty.Props, "className"> & Overrides

   function ComboboxEmpty({ xstyle, className, ...props }: ComboboxEmptyProps) {
     const sx = stylex.props(styles.empty, xstyle)
     return (
       <ComboboxPrimitive.Empty
         data-slot="combobox-empty"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ComboboxSeparatorProps = Omit<ComboboxPrimitive.Separator.Props, "className"> & Overrides

   function ComboboxSeparator({ xstyle, className, ...props }: ComboboxSeparatorProps) {
     const sx = stylex.props(styles.separator, xstyle)
     return (
       <ComboboxPrimitive.Separator
         data-slot="combobox-separator"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ComboboxChipsProps = Omit<ComboboxPrimitive.Chips.Props, "className"> & Overrides

   function ComboboxChips({ xstyle, className, ...props }: ComboboxChipsProps) {
     const sx = stylex.props(
       styles.chips,
       states.focusWithinRing,
       states.fieldSurface,
       styles.chipsDrop,
       xstyle,
     )
     return (
       <ComboboxPrimitive.Chips
         data-slot="combobox-chips"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

   type ComboboxChipProps = Omit<ComboboxPrimitive.Chip.Props, "className" | "style"> &
     StateOverrides & { showRemove?: boolean }

   function ComboboxChip({
     xstyle,
     style,
     className,
     children,
     showRemove = true,
     ...props
   }: ComboboxChipProps) {
     return (
       <ComboboxPrimitive.Chip
         data-slot="combobox-chip"
         {...props}
         {...stateStyles(
           (state) => [
             styles.chip,
             showRemove && styles.chipWithRemove,
             state.disabled && states.disabled,
             xstyle,
           ],
           { className, style },
         )}
       >
         {children}
         {showRemove && (
           <ComboboxPrimitive.ChipRemove
             data-slot="combobox-chip-remove"
             render={<Button variant="ghost" size="icon-xs" xstyle={styles.chipRemove} />}
             aria-label="Remove"
           >
             <X />
           </ComboboxPrimitive.ChipRemove>
         )}
       </ComboboxPrimitive.Chip>
     )
   }

   type ComboboxChipsInputProps = Omit<ComboboxPrimitive.Input.Props, "className"> & Overrides

   function ComboboxChipsInput({ xstyle, className, ...props }: ComboboxChipsInputProps) {
     const sx = stylex.props(styles.chipsInput, xstyle)
     return (
       <ComboboxPrimitive.Input
         data-slot="combobox-chip-input"
         {...sx}
         {...props}
         className={clsx(sx.className, className)}
       />
     )
   }

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

   export {
     Combobox,
     ComboboxChip,
     ComboboxChips,
     ComboboxChipsInput,
     ComboboxClear,
     ComboboxCollection,
     ComboboxContent,
     ComboboxEmpty,
     ComboboxGroup,
     ComboboxInput,
     ComboboxItem,
     ComboboxLabel,
     ComboboxList,
     ComboboxSeparator,
     ComboboxTrigger,
     ComboboxValue,
     comboboxStyles,
     useComboboxAnchor,
   }
   ```

5. Update the import paths to match your project setup.

## Usage

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

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

export function Example() {
  return (
    <Combobox items={frameworks}>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

Pass the options through `items` and render each one from the function child
of `ComboboxList`. Base UI filters `items` against what is typed, so a list
written as static children does not filter and `ComboboxEmpty` never shows.

Give the input an `aria-label` or a `Label`. There is no `Field` to name it.

## Composition

### Simple

```text
Combobox
├── ComboboxInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem
```

### With chips

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

### With groups

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

## Custom items

Pass `itemToStringLabel` when the items are objects. The filter matches against
its result, and the input shows it once an item is picked. Items shaped
`{ value, label }` need neither.

```tsx
type Framework = { label: string; value: string }

const frameworks: Framework[] = [
  { label: "Next.js", value: "next" },
  { label: "SvelteKit", value: "sveltekit" },
  { label: "Nuxt", value: "nuxt" },
]

export function Example() {
  return (
    <Combobox items={frameworks} itemToStringLabel={(framework: Framework) => framework.label}>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(framework: Framework) => (
            <ComboboxItem key={framework.value} value={framework}>
              {framework.label}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Basic

A single input over a flat list.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

export default function ComboboxBasic() {
  return (
    <Combobox items={frameworks}>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Multiple

Set `multiple` and swap the input for `ComboboxChips`. The chosen values render
as `ComboboxChip`s from the function child of `ComboboxValue`, and
`ComboboxChipsInput` filters what is left.

```tsx
import { Fragment } from "react"
import * as stylex from "@stylexjs/stylex"
import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxValue,
  useComboboxAnchor,
} from "@/components/ui/combobox"

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

const styles = stylex.create({
  chips: { width: "100%", maxWidth: "20rem" },
})

export default function ComboboxMultiple() {
  const anchor = useComboboxAnchor()

  return (
    <Combobox multiple autoHighlight items={frameworks} defaultValue={[frameworks[0]]}>
      <ComboboxChips ref={anchor} xstyle={styles.chips}>
        <ComboboxValue>
          {(values: string[]) => (
            <Fragment>
              {values.map((value) => (
                <ComboboxChip key={value}>{value}</ComboboxChip>
              ))}
              <ComboboxChipsInput placeholder="Add framework" aria-label="Frameworks" />
            </Fragment>
          )}
        </ComboboxValue>
      </ComboboxChips>
      <ComboboxContent anchor={anchor}>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

> **Note**
>
> Give `ComboboxChips` the ref from `useComboboxAnchor` and pass the same ref to
> `ComboboxContent` as `anchor`. Base UI otherwise positions against the input,
> which is only part of the frame.

## Clear button

Set `showClear` on the input. The `X` is mounted only while there is a value,
and it replaces the chevron while it is shown.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

export default function ComboboxClear() {
  return (
    <Combobox items={frameworks} defaultValue={frameworks[0]}>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" showClear />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Groups

Pass groups shaped `{ value, items }` to `items`. Render each one as a
`ComboboxGroup` with the group's `items`, a `ComboboxLabel` and a
`ComboboxCollection` that renders the items.

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

const timezones = [
  {
    value: "Americas",
    items: [
      "(GMT-5) New York",
      "(GMT-8) Los Angeles",
      "(GMT-6) Chicago",
      "(GMT-5) Toronto",
      "(GMT-8) Vancouver",
      "(GMT-3) São Paulo",
    ],
  },
  {
    value: "Europe",
    items: [
      "(GMT+0) London",
      "(GMT+1) Paris",
      "(GMT+1) Berlin",
      "(GMT+1) Rome",
      "(GMT+1) Madrid",
      "(GMT+1) Amsterdam",
    ],
  },
  {
    value: "Asia/Pacific",
    items: [
      "(GMT+9) Tokyo",
      "(GMT+8) Shanghai",
      "(GMT+8) Singapore",
      "(GMT+4) Dubai",
      "(GMT+11) Sydney",
      "(GMT+9) Seoul",
    ],
  },
]

type Timezones = (typeof timezones)[number]

export default function ComboboxGroups() {
  return (
    <Combobox items={timezones}>
      <ComboboxInput placeholder="Select a timezone" aria-label="Timezone" />
      <ComboboxContent>
        <ComboboxEmpty>No timezones found.</ComboboxEmpty>
        <ComboboxList>
          {(group: Timezones, index: number) => (
            <ComboboxGroup key={group.value} items={group.items}>
              {index > 0 && <ComboboxSeparator />}
              <ComboboxLabel>{group.value}</ComboboxLabel>
              <ComboboxCollection>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxCollection>
            </ComboboxGroup>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

Filtering drops a group that has no match left, so a `ComboboxSeparator` goes
inside the group rather than between groups. Render it before every group but
the first.

## Custom items

Render anything inside `ComboboxItem`. The tick stays at the trailing edge.

```tsx
import * as stylex from "@stylexjs/stylex"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
import { VStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

type Country = { code: string; label: string; continent: string }

const countries: Country[] = [
  { code: "ar", label: "Argentina", continent: "South America" },
  { code: "au", label: "Australia", continent: "Oceania" },
  { code: "br", label: "Brazil", continent: "South America" },
  { code: "ca", label: "Canada", continent: "North America" },
  { code: "cn", label: "China", continent: "Asia" },
  { code: "eg", label: "Egypt", continent: "Africa" },
  { code: "fr", label: "France", continent: "Europe" },
  { code: "de", label: "Germany", continent: "Europe" },
  { code: "jp", label: "Japan", continent: "Asia" },
  { code: "ke", label: "Kenya", continent: "Africa" },
  { code: "mx", label: "Mexico", continent: "North America" },
  { code: "nz", label: "New Zealand", continent: "Oceania" },
  { code: "za", label: "South Africa", continent: "Africa" },
  { code: "gb", label: "United Kingdom", continent: "Europe" },
  { code: "us", label: "United States", continent: "North America" },
]

const styles = stylex.create({
  title: { whiteSpace: "nowrap", fontWeight: 500 },
  description: { color: color.mutedForeground, fontSize: text.xs, lineHeight: leading.xs },
})

export default function ComboboxCustom() {
  return (
    <Combobox items={countries} itemToStringLabel={(country: Country) => country.label}>
      <ComboboxInput placeholder="Search countries..." aria-label="Country" />
      <ComboboxContent>
        <ComboboxEmpty>No countries found.</ComboboxEmpty>
        <ComboboxList>
          {(country: Country) => (
            <ComboboxItem key={country.code} value={country}>
              <VStack gap={0.5}>
                <span {...stylex.props(styles.title)}>{country.label}</span>
                <span {...stylex.props(styles.description)}>
                  {country.continent} ({country.code})
                </span>
              </VStack>
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Invalid

Set `invalid` on `Field` to style its control, or `aria-invalid` on a bare control. The examples show both.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

function BareControl({ bare = false }: { bare?: boolean }) {
  return (
    <Combobox items={frameworks}>
      <ComboboxInput
        placeholder="Select a framework"
        aria-label="Framework"
        aria-invalid={bare || undefined}
      />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}

export default function InvalidExample() {
  return (
    <VStack gap={5}>
      <BareControl bare />
      <Field invalid>
        <FieldLabel>Framework</FieldLabel>
        <BareControl />
      </Field>
    </VStack>
  )
}
```

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

export default function ComboboxDisabled() {
  return (
    <Field disabled>
      <FieldLabel>Framework</FieldLabel>
      <Combobox items={frameworks}>
        <ComboboxInput placeholder="Select a framework" />
        <ComboboxContent>
          <ComboboxEmpty>No items found.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxContent>
      </Combobox>
    </Field>
  )
}
```

## Auto highlight

Set `autoHighlight` on `Combobox` to highlight the first match as you type, so
Enter picks it.

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

const frameworks = ["Next.js", "SvelteKit", "Nuxt.js", "Remix", "Astro"] as const

export default function ComboboxAutoHighlight() {
  return (
    <Combobox items={frameworks} autoHighlight>
      <ComboboxInput placeholder="Select a framework" aria-label="Framework" />
      <ComboboxContent>
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: string) => (
            <ComboboxItem key={item} value={item}>
              {item}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Popup

Render a `ComboboxTrigger` as a `Button` and move `ComboboxInput` inside
`ComboboxContent`, with `showTrigger={false}`. `ComboboxValue` shows the
current value in the button.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxTrigger,
  ComboboxValue,
} from "@/components/ui/combobox"

type Country = { code: string; label: string }

const countries: Country[] = [
  { code: "", label: "Select country" },
  { code: "ar", label: "Argentina" },
  { code: "au", label: "Australia" },
  { code: "br", label: "Brazil" },
  { code: "ca", label: "Canada" },
  { code: "cn", label: "China" },
  { code: "eg", label: "Egypt" },
  { code: "fr", label: "France" },
  { code: "de", label: "Germany" },
  { code: "jp", label: "Japan" },
  { code: "ke", label: "Kenya" },
  { code: "mx", label: "Mexico" },
  { code: "nz", label: "New Zealand" },
  { code: "za", label: "South Africa" },
  { code: "gb", label: "United Kingdom" },
  { code: "us", label: "United States" },
]

const styles = stylex.create({
  trigger: { width: "16rem", justifyContent: "space-between", fontWeight: 400 },
})

export default function ComboboxPopup() {
  return (
    <Combobox items={countries} defaultValue={countries[0]}>
      <ComboboxTrigger render={<Button variant="outline" xstyle={styles.trigger} />}>
        <ComboboxValue />
      </ComboboxTrigger>
      <ComboboxContent>
        <ComboboxInput showTrigger={false} placeholder="Search" aria-label="Search countries" />
        <ComboboxEmpty>No items found.</ComboboxEmpty>
        <ComboboxList>
          {(item: Country) => (
            <ComboboxItem key={item.code} value={item}>
              {item.label}
            </ComboboxItem>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## Input group

`ComboboxInput` is an `InputGroup`, so an `InputGroupAddon` passed as its child
renders inside the frame.

```tsx
import {
  Combobox,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxInput,
  ComboboxItem,
  ComboboxLabel,
  ComboboxList,
} from "@/components/ui/combobox"
import { InputGroupAddon } from "@/components/ui/input-group"
import { Globe } from "lucide-react"

const timezones = [
  {
    value: "Americas",
    items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago", "(GMT-5) Toronto"],
  },
  {
    value: "Europe",
    items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin", "(GMT+1) Rome"],
  },
  {
    value: "Asia/Pacific",
    items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore", "(GMT+11) Sydney"],
  },
]

type Timezones = (typeof timezones)[number]

export default function ComboboxInputGroup() {
  return (
    <Combobox items={timezones}>
      <ComboboxInput placeholder="Select a timezone" aria-label="Timezone">
        <InputGroupAddon>
          <Globe />
        </InputGroupAddon>
      </ComboboxInput>
      <ComboboxContent>
        <ComboboxEmpty>No timezones found.</ComboboxEmpty>
        <ComboboxList>
          {(group: Timezones) => (
            <ComboboxGroup key={group.value} items={group.items}>
              <ComboboxLabel>{group.value}</ComboboxLabel>
              <ComboboxCollection>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxCollection>
            </ComboboxGroup>
          )}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

Every part wraps the Base UI `Combobox` part of the same name and accepts its
props. See the [Base UI reference](https://base-ui.com/react/components/combobox)
for the full list. Each styled part also takes `xstyle` and `className`.
`ComboboxList`, `ComboboxItem` and `ComboboxChip` take `style` too. It is real
inline CSS, merged over whatever their Base UI state resolves to.

### Combobox

| Prop                | Type                                  | Default |
| ------------------- | ------------------------------------- | ------- |
| `items`             | `readonly Item[] \| readonly Group[]` | —       |
| `multiple`          | `boolean`                             | `false` |
| `value`             | `Item \| Item[] \| null`              | —       |
| `defaultValue`      | `Item \| Item[] \| null`              | —       |
| `onValueChange`     | `(value, details) => void`            | —       |
| `autoHighlight`     | `boolean`                             | `false` |
| `itemToStringLabel` | `(item: Item) => string`              | —       |
| `filter`            | `(item, query, toString) => boolean`  | —       |
| `disabled`          | `boolean`                             | `false` |
| `name`              | `string`                              | —       |

### ComboboxInput

| Prop           | Type      | Default |
| -------------- | --------- | ------- |
| `showTrigger`  | `boolean` | `true`  |
| `showClear`    | `boolean` | `false` |
| `disabled`     | `boolean` | `false` |
| `aria-invalid` | `boolean` | —       |

`xstyle` and `className` apply to the `InputGroup` frame, not the `<input>`.
The popup anchors to the frame, so the popup is the frame's width, addons
included.

### ComboboxContent

| Prop          | Type                                                                       | Default    |
| ------------- | -------------------------------------------------------------------------- | ---------- |
| `side`        | `"top" \| "bottom" \| "left" \| "right" \| "inline-start" \| "inline-end"` | `"bottom"` |
| `sideOffset`  | `number`                                                                   | `6`        |
| `align`       | `"start" \| "center" \| "end"`                                             | `"start"`  |
| `alignOffset` | `number`                                                                   | `0`        |
| `anchor`      | `Element \| RefObject<Element>`                                            | —          |

The popup is never narrower than `9rem` and never taller than `18rem`. The
list inside scrolls.

### ComboboxItem

| Prop       | Type      | Default |
| ---------- | --------- | ------- |
| `value`    | `Item`    | —       |
| `disabled` | `boolean` | `false` |

### ComboboxGroup

| Prop    | Type              | Default |
| ------- | ----------------- | ------- |
| `items` | `readonly Item[]` | —       |

### ComboboxChip

| Prop         | Type      | Default |
| ------------ | --------- | ------- |
| `showRemove` | `boolean` | `true`  |

### comboboxStyles

The style maps behind the component. Spread them onto any element.

| Key               | What it is                               |
| ----------------- | ---------------------------------------- |
| `content`         | The popup box: width, radius, transition |
| `list`            | The scrolling list and its padding       |
| `item`            | An item's row, type and nested corner    |
| `itemHighlighted` | The highlight fill and inset ring        |
| `itemDisabled`    | The dimmed, inert item                   |
| `label`           | A group label                            |
| `empty`           | The empty message                        |
| `separator`       | The hairline between groups              |
| `chips`           | The multi-select frame, ring and surface |
| `chip`            | One chip                                 |
| `chipsInput`      | The bare input inside the chips frame    |

`content` carries no surface. The fill and its hairline ring come from
`popupSurface`. It sets `--carbon-popup-radius`, and `item` merges
`popupItem`, so a row's corner is that radius less the list's inset. A
highlighted item is `itemHighlight` and `itemDisabled` is `disabled`. `chips` is
the frame's box, `focusWithinRing`, `fieldSurface` and the drop with the halo
beside it on `:focus-within`, all from `states` in `theme/states.ts`.

### Data attributes

| Attribute                                  | On               | Effect                                                     |
| ------------------------------------------ | ---------------- | ---------------------------------------------------------- |
| `data-slot="combobox-content"`             | the popup        | Popup surface                                              |
| `data-side`, `data-align`                  | the popup        | Base UI writes them. `data-side` picks the slide direction |
| `data-empty`                               | the popup        | Base UI writes it when the filter leaves nothing           |
| `data-starting-style`, `data-ending-style` | the popup        | Base UI writes them while the popup animates in or out     |
| `data-slot="input-group"`                  | the input frame  | The frame `ComboboxInput` renders                          |
| `data-popup-open`                          | the frame, input | Base UI writes it while the popup is open                  |
| `data-slot="combobox-trigger"`             | the chevron      | What `interop.css` hides beside a clear button             |
| `data-slot="combobox-clear"`               | the clear button | Mounted only while there is a value                        |
| `data-slot="combobox-item"`                | an item          | Targets items from a parent                                |
| `data-highlighted`, `data-selected`        | an item          | Base UI writes them                                        |
| `data-disabled`                            | an item, a chip  | Base UI writes it                                          |
| `data-slot="combobox-chips"`               | the chips frame  | Targets the frame from a parent                            |
| `data-slot="combobox-chip"`                | a chip           | Targets a chip from a parent                               |
