Skip to main content

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

Loading diagram...

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

Loading diagram...

Examples

Combobox

Popover

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').

PropTypeDefaultDescription
itemsTData--(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).
valueTData[number]['value'] (single) or TData[number]['value'][] (multiple)--Controlled selected value. Shape depends on TType.
defaultValueTData[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).
withSearchbooleantrueWhether to show the search input inside the command list.
showSelectedbooleantrueWhether to display the currently selected value(s) in the trigger button. In multiple mode, values beyond 2 are collapsed into a "+N Selected" badge.
commandTriggerPlaceholderstring'Select item...'Placeholder text shown in the trigger button when no value is selected.
commandEmptystring'Nothing found.'Text shown inside the command list when no results match the search query.
popoverReact.ComponentPropsWithoutRef<typeof Popover>--Props spread onto the root Popover component.
popoverTriggerReact.ComponentPropsWithoutRef<typeof Button>--Props spread onto the trigger Button. The variant defaults to 'dashed' when not provided.
popoverContentReact.ComponentPropsWithoutRef<typeof PopoverContent>--Props spread onto PopoverContent. className is merged with the default 'w-(--gentleduck-popover-trigger-width) p-0'.
commandReact.ComponentPropsWithoutRef<typeof Command>--Props spread onto the inner Command component.
commandInputReact.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.

PropTypeDefaultDescription
itemT 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.
checkedboolean--Whether the checkbox indicator shows as checked.
...propsOmit<React.ComponentPropsWithoutRef<typeof CommandItem>, 'onSelect'>--Additional props inherited from CommandItem (excluding onSelect).

ComboxGroup

A convenience wrapper around CommandGroup for grouping combobox items.

PropTypeDefaultDescription
childrenReact.ReactNode--(required) ComboboxItem elements or other content to render inside the group.
...propsReact.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.