combobox
Autocomplete input and command palette with a list of suggestions.
Philosophy
Comboboxes solve the "too many options" problem — they combine a text input with a filterable dropdown. We make the component fully generic (Combobox<TData, TType>) because your data shape shouldn't be our concern. The composition with Popover and Command means you get search, keyboard navigation, and multi-select for free.
How It's Built
Installation
The Combobox is built using a composition of the <Popover /> and the <Command /> components.
See installation instructions for the Popover and the Command components.
Usage
// components/example-combobox.tsx
"use client"
import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
const frameworks = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
]
export function ExampleCombobox() {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? frameworks.find((framework) => framework.value === value)?.label
: "Select framework..."}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === framework.value ? "opacity-100" : "opacity-0"
)}
/>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}// components/example-combobox.tsx
"use client"
import * as React from "react"
import { CheckIcon, ChevronsUpDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover"
const frameworks = [
{
value: "next.js",
label: "Next.js",
},
{
value: "sveltekit",
label: "SvelteKit",
},
{
value: "nuxt.js",
label: "Nuxt.js",
},
{
value: "remix",
label: "Remix",
},
{
value: "astro",
label: "Astro",
},
]
export function ExampleCombobox() {
const [open, setOpen] = React.useState(false)
const [value, setValue] = React.useState("")
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[200px] justify-between"
>
{value
? frameworks.find((framework) => framework.value === value)?.label
: "Select framework..."}
<ChevronsUpDownIcon className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[200px] p-0">
<Command>
<CommandInput placeholder="Search framework..." />
<CommandList>
<CommandEmpty>No framework found.</CommandEmpty>
<CommandGroup>
{frameworks.map((framework) => (
<CommandItem
key={framework.value}
value={framework.value}
onSelect={(currentValue) => {
setValue(currentValue === value ? "" : currentValue)
setOpen(false)
}}
>
<CheckIcon
className={cn(
"mr-2 h-4 w-4",
value === framework.value ? "opacity-100" : "opacity-0"
)}
/>
{framework.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}Note: The Combobox wrapper component (@/components/ui/combobox) uses item values for value, defaultValue, and onValueChange. Use label for display only.
Component Composition
Examples
Combobox
Popover
Dropdown menu
Responsive
You can create a responsive combobox by using the <Popover /> on desktop and the <Drawer /> components on mobile.
Form
API Reference
Combobox<TData, TType>
A generic combobox component built on top of Popover and Command. Supports both single and multiple selection via the TType generic parameter (defaults to 'single').
| Prop | Type | Default | Description |
|---|---|---|---|
items | TData | -- | (required) Readonly array of { label: string; value: string } objects to display as options. |
children | (item: TData) => React.ReactNode | -- | (required) Render function that receives the items array and returns the list content (e.g. ComboboxItem elements). |
value | TData[number]['value'] (single) or TData[number]['value'][] (multiple) | -- | Controlled selected value. Shape depends on TType. |
defaultValue | TData[number]['value'] (single) or TData[number]['value'][] (multiple) | -- | Uncontrolled default value. Shape depends on TType. |
onValueChange | (value) => void | -- | Callback fired when the value changes. Receives a single string (single mode) or string array (multiple mode). |
withSearch | boolean | true | Whether to show the search input inside the command list. |
showSelected | boolean | true | Whether to display the currently selected value(s) in the trigger button. In multiple mode, values beyond 2 are collapsed into a "+N Selected" badge. |
commandTriggerPlaceholder | string | 'Select item...' | Placeholder text shown in the trigger button when no value is selected. |
commandEmpty | string | 'Nothing found.' | Text shown inside the command list when no results match the search query. |
popover | React.ComponentPropsWithoutRef<typeof Popover> | -- | Props spread onto the root Popover component. |
popoverTrigger | React.ComponentPropsWithoutRef<typeof Button> | -- | Props spread onto the trigger Button. The variant defaults to 'dashed' when not provided. |
popoverContent | React.ComponentPropsWithoutRef<typeof PopoverContent> | -- | Props spread onto PopoverContent. className is merged with the default 'w-(--gentleduck-popover-trigger-width) p-0'. |
command | React.ComponentPropsWithoutRef<typeof Command> | -- | Props spread onto the inner Command component. |
commandInput | React.ComponentPropsWithoutRef<typeof CommandInput> | -- | Props spread onto CommandInput when withSearch is true. |
ComboboxItem<T>
A single selectable item inside the combobox list. Renders a CommandItem with a Checkbox indicator.
| Prop | Type | Default | Description |
|---|---|---|---|
item | T extends { label: string; value: string } | -- | (required) The item object. Its label is displayed as text and value is used for selection. |
onSelect | (value: T['value']) => void | -- | Callback fired when this item is selected. Receives the item's value. |
checked | boolean | -- | Whether the checkbox indicator shows as checked. |
...props | Omit<React.ComponentPropsWithoutRef<typeof CommandItem>, 'onSelect'> | -- | Additional props inherited from CommandItem (excluding onSelect). |
ComboxGroup
A convenience wrapper around CommandGroup for grouping combobox items.
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | -- | (required) ComboboxItem elements or other content to render inside the group. |
...props | React.ComponentPropsWithoutRef<typeof CommandGroup> | -- | Additional props inherited from CommandGroup. |
RTL Support
Direction is resolved through the shared primitives direction module. Use a local dir="rtl" override when the component exposes it, or set DirectionProvider at app/root level for global RTL/LTR behavior.