opsinjs
ComponentsActions and forms

Combobox

A text input that narrows a list the product supplies as the reader types, then lets them choose one match.

Preview

Theme
Density
Text
Status
combobox · base base · style base-lyraOpen under the product theme

Installation

pnpm dlx shadcn@latest add @opsinjs/combobox

The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.

Usage

import { Combobox } from "@/components/ui/combobox"
<Combobox
  label="Medication"
  placeholder="Search medications"
  value={medication}
  onValueChange={setMedication}
  items={[
    { value: "amoxicillin", label: "Amoxicillin" },
    { value: "ibuprofen", label: "Ibuprofen" },
    { value: "paracetamol", label: "Paracetamol" },
  ]}
/>

When to use it

Use it when

  • One item from a list long enough that typing a few letters beats scrolling it: a city, a country, a product-owned medication name.
  • A single choice where the reader knows roughly what they want and would rather type than hunt.
  • A field that must stay compact, filtering a long list in place rather than pushing the form down.

Do not use it when

  • A short fixed set that fits in a closed menu, so typing to filter buys nothing.Use select instead.
  • Two to about five short options worth comparing at a glance.Use radio-group instead.

Anatomy

  1. ComboboxThe visible root: a neutral hairline box holding the input and the trigger.Controlled by items
  2. Combobox.InputThe text input and the one tab stop, role="combobox", named by label, filtering by contains match.Controlled by label
  3. Combobox.TriggerA trailing button that opens the popup without typing, a lucide ChevronsUpDown.
  4. Combobox.PopupThe floating list, portalled past overflow clipping, matched to the input's width.
  5. Combobox.ItemOne match, role="option" with aria-selected. The active row takes the neutral hover surface.Controlled by value
  6. Combobox.ItemIndicatorThe lucide Check on the chosen match, so the selection survives greyscale.
  7. Combobox.EmptyThe muted line shown when nothing matches. Base UI keeps it mounted and announces it politely.Controlled by emptyMessage
  • Combobox 1
    • Combobox.Input 1
    • Combobox.Trigger 1
    • Combobox.Popup 1
      • Combobox.Empty 1
      • Combobox.Item 0..n
        • Combobox.ItemIndicator 1

Examples

Filtering a list

items supplies a fictional set of cities and typing narrows it. value starts on one city, so the input is filled and the tick is shown.

Theme
Density
Text
Status
combobox-filtering-a-list · base base · style base-lyraOpen under the product theme

No match

Type something the list does not contain, such as "xyz". emptyMessage fills the popup, announced politely.

Theme
Density
Text
Status
combobox-no-match · base base · style base-lyraOpen under the product theme

Content guidelines

Name the field for the thing chosen, "Medication" rather than "Search", since a screen-reader user hears the name before the role. Write emptyMessage to help, such as "No matches. Check the spelling." Spell item labels as the product's list spells them.

Do

A label of "Medication", a placeholder of "Search medications". The field names the thing, the prompt the act.

Don’t

A label of "Search" and a built-in list of medicines. opsinjs ships no vocabulary.

Accessibility

Audited against WCAG 2.2 AA in a source pass and a rendered pass, author-run rather than independent. Clinical review is pending.

The audit fixed two defects in the source: the trigger button now takes an aria-label built from the field's label, and the input's stray outline-none no longer hides the keyboard focus ring.

  • Base UI renders role="combobox" on the input, role="listbox" on the popup and role="option" with aria-selected on each match.
  • The input is the single tab stop; Arrow keys move a highlight through the popup.
  • The selection is a lucide Check and the highlight a neutral surface under data-highlighted, so both survive greyscale.
  • The input and the trigger floor a 44pt target in rem and carry their own focus ring.

Known gaps. The label reaches a screen reader as aria-label, not a visible label element; a shared repair across the other input wrappers is still open. The live match count is not announced as the reader filters. No contrast pair is measured, so the report below stands in.

KeyActionNotes
TabMoves focus into the control, onto the inputThe one tab stop. A second Tab reaches the trigger.
TypingFilters the list to the matchesContains match on the label. The popup opens as you type.
Arrow DownOpens the popup and moves the highlight into the listOpens a closed popup and highlights the first match.
Arrow Up, Arrow DownMoves the highlight between matchesThe popup holds the highlight while the input keeps the caret.
EnterChooses the highlighted match and closes the popupThe label fills the input and onValueChange reports the value.
EscapeCloses the popup without choosingThe input keeps its text.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below floor
body text on the canvas materiallight104.717.96:1Pass
body text on the canvas materialdark-100.518.00:1Pass
body text on the inset materiallight98.116.32:1Pass
body text on the inset materialdark-100.117.18:1Pass
body text on the card materiallight104.717.96:1Pass
body text on the card materialdark-99.616.32:1Pass
body text on the raised materiallight104.717.96:1Pass
body text on the raised materialdark-97.513.77:1Pass
body text on the sheet material over the darkest backdroplight99.916.76:1Pass
body text on the sheet material over the darkest backdropdark-99.716.47:1Pass
body text on the sheet material over the lightest backdroplight104.717.96:1Pass
body text on the sheet material over the lightest backdropdark-80.86.07:1Pass
body text on the overlay material over the darkest backdroplight85.913.48:1Pass
body text on the overlay material over the darkest backdropdark-99.916.77:1Pass
body text on the overlay material over the lightest backdroplight104.717.96:1Pass
body text on the overlay material over the lightest backdropdark-54.22.69:1Below floor

These are the measured token pairs this component draws colour from, not a measurement of the component itself.

API reference

Prop

Type

Generated from ComboboxProps in registry/bases/base/combobox.tsx.

value and onValueChange make this controlled, with no selection state of its own. A value matching no item renders nothing chosen and raises a development warning. label is required, and items comes from the product. Omitted, emptyMessage falls back to a plain line.

  • Select opens a closed menu of a fixed list with no text filter.
  • RadioGroup shows two to five options side by side.
  • Field is the labelled wrapper a Combobox sits inside.

On this page