Combobox
StableA text input with a filterable list, for choosing from more options than a select can comfortably show.
Installation
$ pnpm dlx shadcn@latest add https://www.prfct.dev/r/combobox.jsonUsage
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxItem,
ComboboxList,
} from "@/components/ui/combobox"const frameworks = ["Next.js", "Remix", "Astro", "SvelteKit"]
<Combobox items={frameworks}>
<ComboboxInput placeholder="Search frameworks…" />
<ComboboxContent>
<ComboboxEmpty>No frameworks found.</ComboboxEmpty>
<ComboboxList>
{(item) => (
<ComboboxItem key={item} value={item}>
{item}
</ComboboxItem>
)}
</ComboboxList>
</ComboboxContent>
</Combobox>Give the root the full items array and render the list with a function: Base UI filters the items as people type and calls the function only for the matches. ComboboxEmpty appears automatically when nothing matches.
Examples
Clearable
showClear swaps the chevron for a clear button once there's a value. Offer it whenever an empty value is valid — an unassigned task, no filter — so people don't have to select the text and delete it.
Groups
Groups are just items with an items array of their own. Render them with ComboboxGroup, a ComboboxLabel heading and a ComboboxCollection for the members; filtering keeps a group only while it has matches. An InputGroupAddon passed as a child adds a leading icon.
Multiple selection
Selected values become chips inside the field. useComboboxAnchor returns a ref for the chips container so the list lines up with the whole field rather than the text input, and autoHighlight makes Enter pick the first match while typing.
Press Backspace in an empty input to remove the last label.
Rich options
Options can show anything that helps people recognize the right one — an avatar and an email disambiguate two people with the same name. With object values, itemToStringLabel decides what fills the input and itemToStringValue what a form submits.
Button trigger
When the field should look like a button — a repository switcher, a toolbar filter — render ComboboxTrigger as a Button and move the input into the popup with showTrigger={false}.
Server-side search
For data that lives on a server, set filter={null} so Base UI stops filtering locally, fetch on onInputValueChange, and pass the results back in as items. Debounce keystrokes, ignore stale responses, and show that a search is in flight — here the search icon becomes a spinner.
Guidelines
When to use
- For long lists — countries, people, time zones, repositories — where typing a few letters is faster than scrolling.
- When people usually know what they're looking for.
- To pick several values from a long list: labels, recipients, tags.
When not to use
- For fewer than about fifteen options people browse rather than search — use a Select.
- For free text with optional suggestions, such as a search box — use an Input with your own suggestion list; a combobox commits to one of its items.
- To run commands — use a Command palette.
Matching
Match anywhere in the label, not only at the start (york should find New York), and ignore case and accents (krakow should find Kraków — pass a locale when your data isn't English). Put the most likely matches first, and keep the empty state specific: say what wasn't found and, when you can, what to do instead.
Accessibility
The input has role="combobox", aria-expanded and aria-autocomplete="list", and controls a listbox popup. Focus stays in the input while the highlight moves through the options, following the ARIA combobox pattern.
| Key | Behavior |
|---|---|
A–Z | Typing filters the list and opens it. |
↓↑ | Opens the list and moves the highlight. At either end, the highlight returns to the input. |
Enter | Chooses the highlighted option. |
Esc | Closes the list; the input keeps its text. |
Backspace | In a multiple combobox with an empty input, removes the last chip. |
Tab | Moves focus to the next control and closes the list. |
- Label the input. Point a visible
FieldLabelat the input'sid, or usearia-labelin toolbars. - Announce loading. When results come from a server, give the spinner an accessible label (the
Spinnercomponent defaults to Loading) and keep the empty state's text meaningful. - Chips. Each chip's remove button is named after the chip — "Remove Design" rather than just "button" — in the language set by LocaleProvider. Pass
removeLabelwhen the chip's content isn't plain text. Remove buttons stay out of the tab order; keyboard users remove chips with Backspace. - Toggle button. The chevron is labelled "Toggle options" and is skipped by Tab — it's a pointer affordance; the input already opens the list from the keyboard.
- Boundaries and targets. The field uses the
inputtoken for its border (3:1, WCAG 1.4.11); options are at least 32px tall.
API reference
Combobox
The root. Doesn't render an element. Accepts every prop of Base UI's Combobox.Root.
ComboboxInput
The text field, rendered inside an Input Group. Children are added to the group, e.g. an InputGroupAddon with an icon.
ComboboxContent
The positioned popup.
ComboboxList, ComboboxItem, ComboboxEmpty
Groups
ComboboxGroup takes the group's items; ComboboxLabel titles it; ComboboxCollection renders its members with a function; ComboboxSeparator divides groups.
Chips
ComboboxChips is the field for multiple selection (attach the ref from useComboboxAnchor()), ComboboxValue renders the selected values with a function, ComboboxChip shows one value with a remove button (showRemove, default true; removeLabel overrides its accessible name), and ComboboxChipsInput is the text input inside the field.
ComboboxTrigger and ComboboxValue
ComboboxTrigger opens the list from a custom element — use render to make it a Button. ComboboxValue renders the current value's label.