Components
Autocomplete input with a list of suggestions, built on @base-ui/react.
Playground
items. ComboboxInput renders an InputGroup with a caret trigger, ComboboxContent portals the popup sized to the anchor, and ComboboxList takes a render function called per matching item. ComboboxEmpty shows when the filter clears everything out.import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@workspace/ui/components/combobox"
const frameworks = ["Next.js", "SvelteKit", "Nuxt", "Remix", "Astro"]
<Combobox items={frameworks}>
<ComboboxInput placeholder="Select a framework" />
<ComboboxContent>
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>showClear on ComboboxInput adds a clear button once a value is selected — it replaces the caret trigger while a selection exists. Set defaultValue for an initial selection, or control it with value / onValueChange on the root.<Combobox items={frameworks} defaultValue={frameworks[0]}>
<ComboboxInput placeholder="Select a framework" showClear />
<ComboboxContent>
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>{ value, items } objects to the root and render each group as a ComboboxGroup — a ComboboxLabel for the heading and a ComboboxCollection with its own render function for the nested items. ComboboxSeparator divides groups; it hides automatically while filtering.const timezones = [
{
value: "Americas",
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-3) São Paulo"],
},
{
value: "Europe",
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
},
// ...
]
<Combobox items={timezones}>
<ComboboxInput placeholder="Select a timezone" />
<ComboboxContent>
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
<ComboboxList>
{(group, index) => (
<ComboboxGroup key={group.value} items={group.items}>
<ComboboxLabel>{group.value}</ComboboxLabel>
<ComboboxCollection>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxCollection>
{index < timezones.length - 1 && <ComboboxSeparator />}
</ComboboxGroup>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>multiple turns the value into an array and keeps the popup open across selections. Selected values render as removable ComboboxChips with a ComboboxChipsInput for typing. The chips container replaces ComboboxInput as the anchor — create a ref with useComboboxAnchor(), attach it to ComboboxChips, and pass it to ComboboxContent. Backspace in the empty input removes the last chip.const anchor = useComboboxAnchor()
<Combobox multiple autoHighlight items={frameworks} defaultValue={[frameworks[0]]}>
<ComboboxChips ref={anchor} className="w-full max-w-72">
<ComboboxValue>
{(values: string[]) => (
<React.Fragment>
{values.map((value) => (
<ComboboxChip key={value}>{value}</ComboboxChip>
))}
<ComboboxChipsInput placeholder="Add framework" />
</React.Fragment>
)}
</ComboboxValue>
</ComboboxChips>
<ComboboxContent anchor={anchor}>
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>render prop and show the selection with ComboboxValue — its placeholder fills in while nothing is selected. Move ComboboxInput inside ComboboxContent with showTrigger={false} so the search field lives in the popup, Select-style.<Combobox items={frameworks}>
<ComboboxTrigger
render={<Button variant="outline" className="w-56 justify-between font-normal" />}
>
<ComboboxValue placeholder="Select a framework" />
</ComboboxTrigger>
<ComboboxContent>
<ComboboxInput showTrigger={false} placeholder="Search frameworks..." />
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>itemToStringValue so filtering and the input display work against a string, and render anything inside ComboboxItem. Here each option is an Item with a title and description; the check indicator still pins to the end edge.type Teammate = { value: string; label: string; role: string }
const teammates: Teammate[] = [
{ value: "ana", label: "Ana Souza", role: "Product Designer" },
{ value: "bruno", label: "Bruno Lima", role: "Frontend Engineer" },
// ...
]
<Combobox
items={teammates}
itemToStringValue={(teammate) => teammate.label}
>
<ComboboxInput placeholder="Assign a teammate" />
<ComboboxContent>
<ComboboxEmpty>No teammates found.</ComboboxEmpty>
<ComboboxList>
{(teammate) => (
<ComboboxItem key={teammate.value} value={teammate}>
<Item size="xs" className="p-0">
<ItemContent>
<ItemTitle>{teammate.label}</ItemTitle>
<ItemDescription>{teammate.role}</ItemDescription>
</ItemContent>
</Item>
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>dir="rtl" ancestor. The popup is portalled out of that ancestor — pass dir="rtl" to ComboboxContent as well so the list mirrors with it.<div dir="rtl">
<Combobox items={cities}>
<ComboboxInput placeholder="اختر مدينة" />
<ComboboxContent dir="rtl">
<ComboboxEmpty>لم يتم العثور على نتائج.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>
</div>role="combobox" with aria-expanded, the popup list is role="listbox", and each option is role="option" with aria-selected. Arrow keys move the highlight, Enter selects, and Escape closes the popup. Give the input an accessible name by pairing it with a Field label or an aria-label.<Field>
<FieldLabel htmlFor="framework">Framework</FieldLabel>
<Combobox items={frameworks}>
<ComboboxInput id="framework" placeholder="Select a framework" />
{/* ... */}
</Combobox>
</Field>useComboboxAnchor helper. All parts accept their underlying Base UI props.| Component | Prop | Default |
|---|---|---|
| Combobox | items?: T[], value?, defaultValue?, onValueChange?, itemToStringValue?: (item: T) => string | — |
| Combobox | multiple?: boolean, autoHighlight?: boolean, openOnInputClick?: boolean, filter?, limit?: number | false / false / true |
| ComboboxInput | showTrigger?: boolean, showClear?: boolean, disabled?: boolean | true / false / false |
| ComboboxContent | side?, align?, sideOffset?, alignOffset?, anchor? | bottom / start / 6 / 0 |
| ComboboxValue | placeholder?: ReactNode, children?: ReactNode | (value) => ReactNode | — |
| ComboboxGroup | items?: T[] | — |
| ComboboxChip | showRemove?: boolean | true |
<ComboboxValue data-slot="combobox-value" /> <ComboboxTrigger data-slot="combobox-trigger" /> <ComboboxClear data-slot="combobox-clear" /> <ComboboxContent data-slot="combobox-content" /> <ComboboxList data-slot="combobox-list" /> <ComboboxItem data-slot="combobox-item" /> <ComboboxGroup data-slot="combobox-group" /> <ComboboxLabel data-slot="combobox-label" /> <ComboboxCollection data-slot="combobox-collection" /> <ComboboxEmpty data-slot="combobox-empty" /> <ComboboxSeparator data-slot="combobox-separator" /> <ComboboxChips data-slot="combobox-chips" /> <ComboboxChip data-slot="combobox-chip" /> <ComboboxChipsInput data-slot="combobox-chip-input" />
Tokens used
16 design tokens consumed by the Combobox component.
--popover
Popover
--popover-foreground
Popover Foreground
--foreground
Foreground
--accent
Accent
--accent-foreground
Accent Foreground
--muted
Muted
--muted-foreground
Muted Foreground
--input
Input
--border
Border
--ring
Ring
--destructive
Destructive
--text-sm
Text Small (14px)
--text-xs
Text Extra Small (12px)
--radius-sm
Radius 0.6× (6px)
--radius-md
Radius 0.8× (8px)
--spacing
Spacing unit (4px)