Skip to content

ComponentsForms

Combo Box

A text input with a filterable list of options. Filters as you type, supports rich items, sections, custom values, async loading, custom filters, controlled input and selection, validation and form submission.

React AriaSource

Installation

pnpm dlx shadcn@latest add @desyne/combobox

The CLI installs dependencies and any other components this one uses.

Usage

tsx
import {
  ComboBox,
  ComboBoxItem,
  ComboBoxItemDescription,
  ComboBoxItemLabel,
  ComboBoxSection,
} from "@/components/ui/combobox";
tsx
<ComboBox label="Country" placeholder="Search countries…" defaultItems={countries}>
  {(country) => <ComboBoxItem>{country.name}</ComboBoxItem>}
</ComboBox>

A combo box has two values: the selected option (value, a Key or null) and the text in the input (inputValue). Choosing an option sets both; typing only changes the text until an option is committed.

defaultItems filters for you, items doesn't

Pass defaultItems (or static children) and the list filters as the user types, using a case- and accent-insensitive "contains" match. Pass items when you filter yourself, for example against a server, and update them from onInputChange.

When to use

  • Combo Box — long lists (roughly 15+) users will search, or when free-form input is allowed.
  • Select — a short list where browsing is faster than typing, and only listed values are valid.
  • Search Field — a query that filters content elsewhere, not a value to pick.
  • Command Palette — a global launcher for actions and navigation.
  • Tag Group with a combo box — picking several values; see the tag picker recipe.

Anatomy

tsx
<ComboBox>                         {/* root: label, input, description, error */}
  <ComboBoxSection title="…">      {/* optional group with a heading */}
    <ComboBoxItem id="…">          {/* option */}
      <Icon />
      <ComboBoxItemLabel />        {/* primary text */}
      <ComboBoxItemDescription />  {/* secondary text */}
    </ComboBoxItem>
  </ComboBoxSection>
</ComboBox>
PartRendersNotes
ComboBox<div> + popoverRoot. Carries data-slot="combobox" and the group/field class. Wraps the label, the input group, description, error message and the list popover.
Input groupFieldGroupThe visible box: prefix, the <input role="combobox">, and a chevron button that toggles the list.
Chevron button<button>Opens the full list. Skipped in the tab order. It's the combo box's React Aria button, so don't put other React Aria buttons in prefix.
PopoverPopover + ListBoxAt least as wide as the input group. Shows emptyMessage when nothing matches (unless allowsEmptyCollection={false}).
ComboBoxItemrole="option"An option. Needs a unique id (or an id on the item object), and textValue when its children aren't a plain string. Shows a check when selected.
ComboBoxItemLabel<span slot="label">Primary text of a rich item.
ComboBoxItemDescription<span slot="description">Secondary text of a rich item.
ComboBoxSectionrole="group"Groups options under an optional title.
Hidden <input><input type="hidden">Rendered when name is set. Submits the selected key (or the text, see form submission).

The input shows the selected option's textValue, so rich items still display as plain text once chosen.

Examples

Variants

outline (default), filled for dense or tinted surfaces, and underlined for minimal forms. The same variants apply to every field component.

Sizes

sm (28px), md (32px, default) and lg (40px), matching the buttons and other fields.

Rich items and prefix

Compose icons or avatars with ComboBoxItemLabel and ComboBoxItemDescription, and set textValue so filtering and the input text use the name. prefix adds an icon to the input.

Sections

Pass sections as defaultItems and render each with ComboBoxSection, giving it items and a render function. Filtering applies to the options inside every section.

Empty state

When nothing matches, the list stays open and shows emptyMessage (default "No results"), so users know the search worked. Type something that isn't in the list below to see it. Pass allowsEmptyCollection={false} to close the list instead.

menuTrigger controls when the list opens: input (default) when the user types, focus as soon as the input is focused, which suits short lists of suggestions, and manual only from the chevron button or the arrow keys.

Custom values

allowsCustomValue accepts text that isn't in the list. The value stays null for custom text, so read inputValue (or the submitted form value) instead.

Type anything to use a new label.

Value: —

Controlled

Control the selection with value / onChange and the text with inputValue / onInputChange. When you control both, update the input text yourself when the selection changes, and reset both to clear the field.

value: EUR · input: “Euro”

Async loading

React Aria's useAsyncList loads items as the user types and aborts stale requests. Wire its filterText to inputValue and pass the results as items. Swapping the prefix for a Spinner shows progress in place.

Custom filter

defaultFilter replaces the built-in "contains" match. React Aria's useFilter gives you locale-aware startsWith, contains and endsWith.

Matches from the start: “git p” finds pull and push.

Disabled

isDisabled disables the input and the button; disabledKeys keeps specific options visible but unselectable. Use description to explain why.

Enterprise requires a sales call.

Required and validation

isRequired blocks form submission until an option is selected and shows the browser's message. Unless allowsCustomValue is set, the input reverts to the selected option's text on blur, so the value is always a listed option.

Custom validation

validate receives { value, inputValue } and returns an error string or null. With validationBehavior="aria" on the Form, errors show live as the selection changes.

The database needs at least 4 GB of memory.t3.micro is too small for Postgres.

Form submission

With a name, the selected key is submitted through a hidden input. Set formValue="text" to submit the input text instead; with allowsCustomValue, the text is always submitted.

Recipes

Tag picker

Multiple selection built from a combo box and a TagGroup. The combo box stays empty (value={null}), each choice is added as a removable tag, and chosen options drop out of the list.

React
TypeScript

Dependent address fields

A country combo box drives the options, label and reset of the region combo box, alongside text fields with autoComplete hints.

Shipping address

Accessibility

  • The input has role="combobox" with aria-expanded, aria-controls and aria-activedescendant, so focus stays in the input while options are highlighted.
  • Always provide a label: label, aria-label, or aria-labelledby.
  • Screen readers announce the number of available options as the list changes, and the selected option when it's committed.
  • description and errorMessage are linked to the input with aria-describedby.
  • The chevron button is skipped in the tab order because the arrow keys open the list from the input.
  • Items that aren't plain strings need textValue so they can be filtered, announced and shown in the input.

Keyboard

KeyAction
TypingFilters the list and opens it (with menuTrigger="input")
↓ / ↑Opens the list, focusing the first / last option; then moves between options
EnterSelects the focused option and closes the list
TabSelects the focused option (if the list is open) and moves focus on
EscCloses the list and reverts the input to the selected option's text
← / →Moves the text cursor, leaving the list

Styling

Data attributes

On the ComboBox root (use group-data-*/field: to style children):

AttributePresent when
data-openThe list is open
data-focusedThe input has focus
data-disabledisDisabled is true
data-readonlyisReadOnly is true
data-invalidValidation failed
data-requiredisRequired is true

On each ComboBoxItem:

AttributePresent when
data-focused / data-hoveredThe option is highlighted / hovered
data-selectedThe option is the current value
data-disabledThe option is in disabledKeys

Slots

data-slotElement
comboboxRoot
label, description, field-errorField text
field-group, field-inputInput box and <input>
popover, list-boxOverlay and list
list-box-item, list-box-sectionOptions and groups

The popover is at least as wide as the input group (min-w-(--trigger-width)). Pass a className on ComboBox to size the whole field; it also accepts a function of the render state (isOpen, isDisabled, isInvalid, isReadOnly, isRequired). Option styles come from listBoxItemStyles in list-box.tsx.

API Reference

ComboBox

Prop

Type

Also accepts every prop of React Aria's ComboBox. This wrapper is single-selection only.

ComboBoxItem

Prop

Type

ComboBoxSection

Prop

Type

ComboBoxItemLabel / ComboBoxItemDescription

Accept every prop of React Aria's Text. They set slot="label" and slot="description" so each option's accessible name and description are wired up automatically.

  • Select — the same list without typing.
  • List Box — the list on its own, always visible, with multiple selection.
  • Tag Group — for showing several chosen values.
  • Search Field — for queries rather than values.