Combobox
Searchable select with typeahead.
Preview
Code
import {
Combobox, ComboboxContent, ComboboxEmpty, ComboboxInput,
ComboboxItem, ComboboxList,
} from "@/components/ui/combobox";
const items = [
"C3 product",
"Enterprise product",
"Mobile app",
"Internal tool",
"Marketing site",
];
export function Example() {
return (
<Combobox items={items}>
<ComboboxInput placeholder="Search products…" className="w-56" />
<ComboboxContent>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
<ComboboxEmpty>No results.</ComboboxEmpty>
</ComboboxContent>
</Combobox>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled selected value. |
| defaultValue | string | — | Uncontrolled initial value. |
| onValueChange | (value: string) => void | — | Called when the selection changes. |
| items | readonly any[] | — | Items to filter against the input query. Pair with a render-function child on ComboboxList to render each filtered item. |
Sub-components
ComboboxInput— The search input that is both the trigger and the query field. Focus or type to open the popup; the input's value drives filtering against the `items` prop.| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
ComboboxContent— The popup with the filterable list. Portals to document body.| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
ComboboxList— The list container. Accepts either static children or a render function `(item) => ReactNode` when `items` is set on the root.| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
ComboboxItem— An individual option.| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | The value this item represents. |
ComboboxEmpty— Rendered when no items match the query.| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
ComboboxTrigger— Optional Select-style button trigger. Use instead of `ComboboxInput` when you want a closed dropdown that opens on click rather than a typeahead-first input.| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | — | Additional Tailwind classes, merged with the component's defaults via cn(). |
Built on Base UI Combobox. The default pattern is typeahead-first: `ComboboxInput` is the visible control and filters items as the user types. Use `ComboboxTrigger` only for a Select-style closed state.
All components also accept standard HTML attributes for their root element (e.g. `id`, `aria-*`, `data-*`, event handlers) and forward `ref` where applicable.
Source
The full implementation of components/ui/combobox.tsx is below — copy it into your project and own it. Some components import the cn helper from lib/utils or other primitives; copy those too.
"use client";
import { cn } from "@/lib/utils";
import { Combobox as BaseCombobox } from "@base-ui-components/react/combobox";
import { Check, ChevronsUpDown } from "lucide-react";
import type { ComponentPropsWithoutRef } from "react";
export const Combobox = BaseCombobox.Root;
export function ComboboxTrigger({
className,
children,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.Trigger>) {
return (
<BaseCombobox.Trigger
className={cn(
"flex h-9 w-full items-center justify-between rounded-md border border-border bg-bg px-3 py-2",
"text-sm text-fg shadow-sm",
"hover:bg-bg-subtle focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-accent",
"disabled:pointer-events-none disabled:opacity-50",
"data-[popup-open]:outline-2 data-[popup-open]:outline-offset-2 data-[popup-open]:outline-accent",
className,
)}
{...props}
>
{children ?? (
<>
<BaseCombobox.Value>
{(v: unknown) => (v != null ? String(v) : "Select…")}
</BaseCombobox.Value>
<BaseCombobox.Icon>
<ChevronsUpDown className="h-4 w-4 opacity-50" />
</BaseCombobox.Icon>
</>
)}
</BaseCombobox.Trigger>
);
}
export function ComboboxInput({
className,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.Input>) {
return (
<BaseCombobox.Input
className={cn(
"flex h-9 w-full rounded-md border border-border bg-bg px-3 py-2",
"text-sm text-fg shadow-sm placeholder:text-fg-subtle",
"focus-visible:outline-1 focus-visible:outline-offset-0 focus-visible:outline-accent",
"disabled:pointer-events-none disabled:opacity-50",
className,
)}
{...props}
/>
);
}
export function ComboboxContent({
className,
sideOffset = 4,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.Popup> & { sideOffset?: number }) {
return (
<BaseCombobox.Portal>
<BaseCombobox.Positioner sideOffset={sideOffset} className="z-50">
<BaseCombobox.Popup
className={cn(
"max-h-[18rem] w-(--anchor-width) overflow-y-auto overscroll-contain rounded-md border border-border bg-surface-elevated shadow-md",
"data-[starting-style]:opacity-0 data-[starting-style]:scale-95",
"data-[ending-style]:opacity-0 data-[ending-style]:scale-95",
"transition-all duration-150",
className,
)}
{...props}
/>
</BaseCombobox.Positioner>
</BaseCombobox.Portal>
);
}
export function ComboboxList({
className,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.List>) {
return <BaseCombobox.List className={cn("p-1", className)} {...props} />;
}
export function ComboboxItem({
className,
children,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.Item>) {
return (
<BaseCombobox.Item
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm text-fg outline-none",
"data-[highlighted]:bg-bg-muted data-[highlighted]:text-fg",
"data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className,
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<BaseCombobox.ItemIndicator>
<Check className="h-3.5 w-3.5" />
</BaseCombobox.ItemIndicator>
</span>
{children}
</BaseCombobox.Item>
);
}
export function ComboboxEmpty({
className,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.Empty>) {
return (
<BaseCombobox.Empty
className={cn("py-6 text-center text-sm text-fg-muted empty:hidden", className)}
{...props}
/>
);
}
export function ComboboxGroup(props: ComponentPropsWithoutRef<typeof BaseCombobox.Group>) {
return <BaseCombobox.Group {...props} />;
}
export function ComboboxGroupLabel({
className,
...props
}: ComponentPropsWithoutRef<typeof BaseCombobox.GroupLabel>) {
return (
<BaseCombobox.GroupLabel
className={cn("px-2 py-1.5 text-xs font-semibold text-fg-muted", className)}
{...props}
/>
);
}JavaFX
Ships in the PRIZM JavaFX library for thick-client C3 apps as PrizmCombobox<T> (extends Region (custom: search field + filtered popup list)). Run the gallery to see it natively.
import design.prizm.fx.controls.PrizmCombobox<T>;
PrizmCombobox()
PrizmCombobox(ObservableList<T> items)| Prop | Type | Default | Description |
|---|---|---|---|
| getSourceItems | () → ObservableList<T> | — | The backing options — add / remove here. |
| value | ObjectProperty<T> | — | The committed value (what the check marks); changes only on Enter / click. |
| setPromptText | (String) → void | — | Placeholder for the search field. |
A custom control (not the stock editable ComboBox, whose arrow-keys mutate the value). A search field filters a popup list; the single highlight (selected row) follows keyboard AND hover, while the check marks only the committed value and never moves during navigation. Enter / click commits. The non-editable picker is PrizmSelect. Mirrors components/ui/combobox.tsx.