# Combobox

Search and choose from known options.

Assignee picker

[Complete TSX file](/examples/combobox/default.tsx)

```tsx
"use client";

import { UserIcon } from "lucide-react";
import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";

const people = [
  "Mara Ellis",
  "Ben Carter",
  "Sofia Nguyen",
  "Priya Shah",
  "Nora Lind",
  "Sam Ortiz",
];

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Assignee</FieldLabel>
      <Combobox items={people}>
        <ComboboxInput
          className="w-full"
          placeholder="Select a person"
          startAddon={<UserIcon />}
        />
        <ComboboxPopup>
          <ComboboxEmpty>No people found.</ComboboxEmpty>
          <ComboboxList>
            {(person: string) => (
              <ComboboxItem key={person} value={person}>
                {person}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
    </Field>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/combobox
```

With npm: `npx shadcn@4.18.0 add @siteplane/combobox`. [Registry JSON](/registry/combobox.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input @siteplane/select
```

Field is installed with Input.

## Usage

```tsx
import {
  Combobox,
  ComboboxInput,
  ComboboxPopup,
  ComboboxEmpty,
  ComboboxList,
  ComboboxItem,
} from "@/siteplane/components/combobox";

<Combobox items={["Design"]}>
  <ComboboxInput aria-label="Workspace" />
  <ComboboxPopup>
    <ComboboxEmpty>No matching workspaces.</ComboboxEmpty>
    <ComboboxList>
      {(item: string) => (
        <ComboboxItem key={item} value={item}>
          {item}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxPopup>
</Combobox>;
```

An item shows its check in the leading slot. For identity rows that lead with an avatar or a logo, such as the people of an assignee picker, set `indicatorPosition="end"` on `ComboboxItem`: the row starts with its first child and ends with the check, and its label keeps one line, as on [Menu](/docs/components/menu/#checkbox-and-radio-items) radio items.

## Composition

```text
Combobox
├── ComboboxInput
└── ComboboxPopup
    ├── ComboboxStatus (optional)
    ├── ComboboxEmpty
    └── ComboboxList → ComboboxItem
```

## Controlled value and clear

The clear button updates the controlled value. Type a non-matching query to see the empty result.

Controlled value and clear

[Complete TSX file](/examples/combobox/controlled.tsx)

```tsx
"use client";

import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState<string | null>("Design");
  return (
    <div className="w-full max-w-sm">
      <Combobox
        items={["Design", "Engineering", "Research"]}
        value={value}
        onValueChange={setValue}
      >
        <ComboboxInput
          aria-label="Workspace"
          placeholder="Find workspace"
          showClear
        />
        <ComboboxPopup>
          <ComboboxEmpty>No matching workspaces.</ComboboxEmpty>
          <ComboboxList>
            {(item: string) => (
              <ComboboxItem key={item} value={item}>
                {item}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
    </div>
  );
}
```

## Multiple selection

Set `multiple` and render the selected values as `ComboboxChip`s inside `ComboboxChips`; each chip includes its own remove button. Use `removeProps` to give each remove button a specific name. Render the chips through `ComboboxValue`, as below: a chip that is added fades in, a removed chip fades out where it was, and the chips and the input after it glide to their new place. Reduced motion changes them at once. The field is as tall as an Input of the `ComboboxChipsInput`'s size, and its chips are 20 px at `sm`, 24 px at `md` and `lg` and 32 px at `xl`. A chip is never wider than the field: a long value ends with an ellipsis and keeps its remove button, and the full value is the label's title, as on a [Tag](/docs/components/tag-group/). The check of a selected item draws in when the item is picked and out when it is not, the way the [Checkbox](/docs/components/checkbox/) check does, and appears instantly under reduced motion.

Label picker with chips

[Complete TSX file](/examples/combobox/multiple.tsx)

```tsx
"use client";

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxChipsInput,
  ComboboxEmpty,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
  ComboboxValue,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";

const labels = [
  "Bug",
  "Feature",
  "Design",
  "Docs",
  "Frontend",
  "Backend",
  "Research",
];

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Labels</FieldLabel>
      <Combobox defaultValue={["Feature", "Frontend"]} items={labels} multiple>
        <ComboboxChips>
          <ComboboxValue>
            {(value: string[]) => (
              <>
                {value.map((label) => (
                  <ComboboxChip
                    key={label}
                    removeProps={{ "aria-label": `Remove ${label}` }}
                  >
                    {label}
                  </ComboboxChip>
                ))}
                <ComboboxChipsInput
                  placeholder={value.length > 0 ? undefined : "Add labels"}
                />
              </>
            )}
          </ComboboxValue>
        </ComboboxChips>
        <ComboboxPopup>
          <ComboboxEmpty>No labels found.</ComboboxEmpty>
          <ComboboxList>
            {(label: string) => (
              <ComboboxItem key={label} value={label}>
                {label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
    </Field>
  );
}
```

## Grouped options

Pass groups as `items` and render them with `ComboboxGroup` and `ComboboxCollection`. `itemToStringLabel` turns each object item into the text shown in the input and used for filtering.

Timezone combobox with groups

[Complete TSX file](/examples/combobox/groups.tsx)

```tsx
"use client";

import {
  Combobox,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";

type Timezone = { label: string; value: string };

const timezones: { value: string; items: Timezone[] }[] = [
  {
    value: "Americas",
    items: [
      { label: "(GMT-8) Los Angeles", value: "America/Los_Angeles" },
      { label: "(GMT-6) Chicago", value: "America/Chicago" },
      { label: "(GMT-5) New York", value: "America/New_York" },
    ],
  },
  {
    value: "Europe",
    items: [
      { label: "(GMT+0) London", value: "Europe/London" },
      { label: "(GMT+1) Berlin", value: "Europe/Berlin" },
      { label: "(GMT+1) Paris", value: "Europe/Paris" },
    ],
  },
  {
    value: "Asia/Pacific",
    items: [
      { label: "(GMT+8) Singapore", value: "Asia/Singapore" },
      { label: "(GMT+9) Tokyo", value: "Asia/Tokyo" },
      { label: "(GMT+10) Sydney", value: "Australia/Sydney" },
    ],
  },
];

export default function Example() {
  return (
    <div className="w-full max-w-sm">
      <Combobox
        items={timezones}
        itemToStringLabel={(timezone: Timezone) => timezone.label}
      >
        <ComboboxInput aria-label="Timezone" placeholder="Select a timezone" />
        <ComboboxPopup>
          <ComboboxEmpty>No timezones found.</ComboboxEmpty>
          <ComboboxList>
            {(group: (typeof timezones)[number]) => (
              <ComboboxGroup key={group.value} items={group.items}>
                <ComboboxGroupLabel>{group.value}</ComboboxGroupLabel>
                <ComboboxCollection>
                  {(timezone: Timezone) => (
                    <ComboboxItem key={timezone.value} value={timezone}>
                      {timezone.label}
                    </ComboboxItem>
                  )}
                </ComboboxCollection>
              </ComboboxGroup>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
    </div>
  );
}
```

## Searchable select

When a long list needs search but the field should look like a Select, render `ComboboxTrigger` with `SelectButton` and put an embedded `ComboboxInput` inside the popup.

Country picker with search

[Complete TSX file](/examples/combobox/searchable-select.tsx)

```tsx
"use client";

import { SearchIcon } from "lucide-react";
import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
  ComboboxTrigger,
  ComboboxValue,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { SelectButton } from "@/siteplane/components/select";

const countries = [
  { label: "Australia", value: "au" },
  { label: "Brazil", value: "br" },
  { label: "Canada", value: "ca" },
  { label: "France", value: "fr" },
  { label: "Germany", value: "de" },
  { label: "India", value: "in" },
  { label: "Japan", value: "jp" },
  { label: "Netherlands", value: "nl" },
  { label: "Spain", value: "es" },
  { label: "United Kingdom", value: "gb" },
  { label: "United States", value: "us" },
];

export default function Example() {
  return (
    <Field className="w-full max-w-sm">
      <FieldLabel>Country</FieldLabel>
      <Combobox items={countries}>
        <ComboboxTrigger render={<SelectButton />}>
          <ComboboxValue placeholder="Select country" />
        </ComboboxTrigger>
        <ComboboxPopup aria-label="Select country">
          <div className="p-2">
            <ComboboxInput
              placeholder="Search countries…"
              profile="embedded"
              showTrigger={false}
              startAddon={<SearchIcon />}
            />
          </div>
          <ComboboxEmpty>No countries found.</ComboboxEmpty>
          <ComboboxList>
            {(country: (typeof countries)[number]) => (
              <ComboboxItem key={country.value} value={country}>
                {country.label}
              </ComboboxItem>
            )}
          </ComboboxList>
        </ComboboxPopup>
      </Combobox>
    </Field>
  );
}
```

## Variants and disabled

`variant` belongs to the input. Disabled prevents input interaction.

Combobox variants and disabled state

[Complete TSX file](/examples/combobox/appearance.tsx)

```tsx
"use client";

import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";

const workspaces = ["Design", "Engineering", "Research"];

const inputs: {
  disabled?: boolean;
  label: string;
  variant?: "default" | "elevated" | "inset";
}[] = [
  { label: "default", variant: "default" },
  { label: "elevated", variant: "elevated" },
  { label: "inset", variant: "inset" },
  { label: "disabled", disabled: true },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {inputs.map(({ disabled, label, variant }) => (
        <Field disabled={disabled} key={label}>
          <FieldLabel>Workspace, {label}</FieldLabel>
          <Combobox items={workspaces}>
            <ComboboxInput placeholder="Find workspace" variant={variant} />
            <ComboboxPopup>
              <ComboboxEmpty>No matching workspaces.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </Combobox>
        </Field>
      ))}
    </div>
  );
}
```

## Sizes

`size` belongs to the input. Omit it to inherit the provider's control size.

Combobox sizes

[Complete TSX file](/examples/combobox/sizes.tsx)

```tsx
"use client";

import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { Field, FieldLabel } from "@/siteplane/components/field";

const workspaces = ["Design", "Engineering", "Research"];

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {sizes.map(({ size, label }) => (
        <Field key={size}>
          <FieldLabel>Workspace, {label}</FieldLabel>
          <Combobox items={workspaces}>
            <ComboboxInput placeholder="Find workspace" size={size} />
            <ComboboxPopup>
              <ComboboxEmpty>No matching workspaces.</ComboboxEmpty>
              <ComboboxList>
                {(item: string) => (
                  <ComboboxItem key={item} value={item}>
                    {item}
                  </ComboboxItem>
                )}
              </ComboboxList>
            </ComboboxPopup>
          </Combobox>
        </Field>
      ))}
    </div>
  );
}
```

## Form integration

Place the combobox in a `Field` with a `name`; `required` blocks submission until a value is chosen and `FieldError` explains why. For object items, `itemToStringValue` decides the submitted string.

Workspace field in a form

[Complete TSX file](/examples/combobox/form.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/siteplane/components/combobox";
import { Field, FieldError, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";

const workspaces = ["Design", "Engineering", "Research"];

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-3"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field name="workspace">
        <FieldLabel>Default workspace</FieldLabel>
        <Combobox items={workspaces} required>
          <ComboboxInput className="w-full" placeholder="Find workspace" />
          <ComboboxPopup>
            <ComboboxEmpty>No matching workspaces.</ComboboxEmpty>
            <ComboboxList>
              {(item: string) => (
                <ComboboxItem key={item} value={item}>
                  {item}
                </ComboboxItem>
              )}
            </ComboboxList>
          </ComboboxPopup>
        </Combobox>
        <FieldError>Choose a default workspace.</FieldError>
      </Field>
      <Button type="submit">Save</Button>
    </Form>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| itemToStringLabel | (item: Value) => string | label for { value, label } items | Convert an object item to input display and filtering text. |
| itemToStringValue | (item: Value) => string | value for { value, label } items | Convert an object item to its native form submission value; does not set display text. |
| ComboboxInput.size | sm \| md \| lg \| xl \| number | provider / lg | Visual size, or numeric native input size. |
| ComboboxInput.variant | default \| elevated \| inset | default | Input surface. |
| ComboboxInput.showTrigger | boolean | true | Show popup toggle. |
| ComboboxInput.showClear | boolean | false | Show clear action. |
| ComboboxInput.startAddon | ReactNode | unset | Leading content. |
| ComboboxInput.triggerProps / clearProps | Trigger.Props / Clear.Props | unset | Configure generated action buttons. |
| ComboboxPopup.side / align | Positioner side / alignment | bottom / start | Preferred position. |
| ComboboxPopup.sideOffset / alignOffset | offsets | 4 / unset | Gap and alignment correction. |
| ComboboxPopup.anchor | Positioner anchor | trigger/input or chips for multiple | Custom anchor. |
| ComboboxPopup.portalProps | Portal.Props | unset | Portal options. |
| ComboboxItem.indicatorPosition | start \| end | start | Where the check shows: in the leading slot, or at the end of an identity row that leads with an avatar or a logo. |
| value / defaultValue | selected Value or Value[] | uncontrolled | Selection |
| onValueChange | (value, eventDetails) => void | unset | Value callback. |
| onOpenChange | (open, eventDetails) => void | unset | Popup visibility callback. |
| ComboboxInput.profile | ComboboxInputProfile: default \| embedded | default | Standard or embedded input surface. |
| ComboboxChips.layout | input \| standalone | input | Input surface, or a surface-free chip list below a separate ComboboxInput. |
| ComboboxChips.startAddon | ReactNode | unset | Leading chip-container content. |
| ComboboxChips.variant | default \| elevated \| inset | default | Chip-container surface. |
| ComboboxChipsInput.size | control size \| number | provider / lg | Input dimensions; the chips field and its chips follow them. |
| ComboboxChip.removeProps | ChipRemove.Props | unset | Generated remove control props. |

Combobox preserves Base UI’s Value/Multiple generics and provides its context for chips/input anchoring. ComboboxInputProps omits the foundation render prop; use the documented embedded profile or public composition parts instead. ComboboxInputProfile is exported. ComboboxChips, Chip, ChipsInput and ChipRemove wrap the matching foundation parts; Chip supplies its own removal action by default. ComboboxContext exports chipsRef/inputRef (null) and multiple (false) defaults for the composition. Item forwards pointer events through its touch-selection guards. `inputValue`/`onInputValueChange` control the query independently from selected value. `useComboboxFilter` is the foundation filtering hook.

ComboboxList renders the filtered collection; Item represents a suggestion; Empty displays no results; Status provides status feedback. ComboboxGroup, ComboboxGroupLabel, ComboboxSeparator and ComboboxRow structure options. ComboboxValue renders the current value; ComboboxCollection supplies foundation collection composition. ComboboxTrigger and ComboboxClear are independently exported actions; their default render uses kit Buttons, and ComboboxTrigger also accepts `render={<SelectButton />}` for a select-style trigger. Popup includes Portal and Positioner; ComboboxList adds the ScrollArea. ComboboxPrimitive exports the namespace. Remaining root item-to-string/filter, keyboard, validation, disabled, readOnly, form, events and refs follow [Base UI Combobox](https://base-ui.com/react/components/combobox). Styled className props are strings. No independent fetch or loading service is supplied.

`ComboboxTrigger` and `ComboboxClear` accept their corresponding foundation props and default to a ghost Button with field-action profile and icon-xs size. Supply an accessible name when composing either action separately. `ComboboxGroup`, `ComboboxGroupLabel`, `ComboboxRow`, `ComboboxSeparator`, `ComboboxValue`, and `ComboboxCollection` accept their matching foundation part props with kit styling; they add no owned state API.

`ComboboxChipRemove` accepts foundation ChipRemove props and defaults to a bare `icon-2xs` text Button with an X icon and the accessible label “Remove”. Override the label through removeProps on ComboboxChip for additional context. Do not add a second removal button: Chip already renders this part.

[Read the installed source](/source/combobox).

## Usage notes

Arrow keys navigate suggestions, Enter accepts and Escape dismisses. Keep a visible label in a production form. For async data, the application owns fetching, stale-result handling and pending/error status; render those states through Status and the surrounding field. See [Choosing selection controls](/docs/guides/selection/).
