Light

Combobox

Autocomplete input and command palette with a list of suggestions.

Spec · from metadata

When to use

  • Searchable dropdown selection from a large list
  • Autocomplete/typeahead input patterns
  • Multi-select with tag/chip display
  • Select with grouped or filterable options

When not to use

  • Small static list (< 5 options) — use Select or RadioGroup
  • Command palette / keyboard-driven menu — use Command instead
  • Free-form text input — use Input or Textarea instead

Anti-patterns

Avoid<InputGroup>
  <ComboboxInput />
</InputGroup>
Prefer<ComboboxInput placeholder="Search..." />

ComboboxInput already wraps InputGroup internally. Nesting creates double borders.

Avoid<Combobox>
  <ComboboxInput />
  <Portal>
    <ComboboxContent>...</ComboboxContent>
  </Portal>
</Combobox>
Prefer<Combobox>
  <ComboboxInput />
  <ComboboxContent>...</ComboboxContent>
</Combobox>

ComboboxContent handles its own Portal internally. Do not wrap in another Portal.

Accessibility

  • Required ARIAaria-label or associated label
  • Screen readerBuilt on Base UI Combobox which manages combobox ARIA pattern. Selected items show a check indicator.
  • ContrastHighlighted items use bg-accent. Selected items also use bg-accent with text-accent-foreground.

Token bindings

TokenCategoryUsage
popovercolorPopup background
popover-foregroundcolorPopup text
accentcolorHighlighted/selected item
mutedcolorChip background
input-bgcolorChips container background
rounded-mdradiusPopup border radius
shadow-mdshadowPopup shadow

Import

import {
  Combobox,
  ComboboxInput,
  ComboboxContent,
  ComboboxList,
  ComboboxItem,
  ComboboxGroup,
  ComboboxLabel,
  ComboboxCollection,
  ComboboxEmpty,
  ComboboxSeparator,
  ComboboxChips,
  ComboboxChip,
  ComboboxChipsInput,
  ComboboxTrigger,
  ComboboxValue,
  useComboboxAnchor,
} from "@timelycare/helix-ui"

Props

// Combobox — Root wrapper (re-exports ComboboxPrimitive.Root from @base-ui/react)
interface ComboboxProps extends ComboboxPrimitive.Root.Props {
  value?: string | string[]
  defaultValue?: string | string[]
  onValueChange?: (value: string | string[]) => void
  open?: boolean
  onOpenChange?: (open: boolean) => void
  disabled?: boolean
  multiple?: boolean
}

// ComboboxInput — Search input with optional trigger/clear buttons
interface ComboboxInputProps extends ComboboxPrimitive.Input.Props {
  showTrigger?: boolean   // default: true — renders chevron trigger button
  showClear?: boolean     // default: false — renders X clear button
  disabled?: boolean      // default: false
}

// ComboboxContent — Popup container with positioning
interface ComboboxContentProps extends ComboboxPrimitive.Popup.Props {
  side?: "top" | "bottom" | "left" | "right"   // default: "bottom"
  sideOffset?: number                            // default: 6
  align?: "start" | "center" | "end"            // default: "start"
  alignOffset?: number                           // default: 0
  anchor?: React.RefObject<HTMLElement | null> | HTMLElement | null
}

// ComboboxItem — Individual option
interface ComboboxItemProps extends ComboboxPrimitive.Item.Props {
  value: string
  disabled?: boolean
  className?: string
  children: React.ReactNode
}

// ComboboxChip — Tag/badge for multi-select
interface ComboboxChipProps extends ComboboxPrimitive.Chip.Props {
  showRemove?: boolean   // default: true — renders X remove button
}

// ComboboxChipsInput — Inline input inside the chips container
interface ComboboxChipsInputProps extends ComboboxPrimitive.Input.Props {}

// ComboboxGroup, ComboboxLabel, ComboboxList, ComboboxCollection,
// ComboboxEmpty, ComboboxSeparator, ComboboxTrigger, ComboboxValue
// all pass through their respective ComboboxPrimitive.*.Props

// useComboboxAnchor — hook returning a ref for anchoring the popup to a custom element
function useComboboxAnchor(): React.RefObject<HTMLDivElement | null>

Structure

PartTailwind
ComboboxInput (wrapper)w-auto (InputGroup) — input renders via InputGroupInput
Trigger buttonsize-icon-xs variant-ghost via InputGroupButton, icon size-4 text-muted-foreground
Clear buttonsize-icon-xs variant-ghost via InputGroupButton, icon XIcon
ComboboxContent (Positioner)isolate z-50
ComboboxContent (Popup)bg-popover text-popover-foreground rounded-md shadow-md ring-1 ring-foreground/10 max-h-96 w-(--anchor-width) min-w-[calc(var(--anchor-width)+--spacing(7))]
ComboboxListscroll-py-1 overflow-y-auto p-1 data-empty:p-0 with max-height clamped to available space
ComboboxItempy-1.5 pr-8 pl-2 gap-2 rounded-sm text-sm with CheckIcon indicator at absolute right-2
ComboboxGrouppass-through (no default styles)
ComboboxLabelpx-2 py-1.5 text-xs text-muted-foreground (coarse: px-3 py-2 text-sm)
ComboboxEmptytext-muted-foreground text-sm text-center py-2 — hidden until data-empty on content
ComboboxSeparatorbg-border -mx-1 my-1 h-px
ComboboxChipsmin-h-9 flex-wrap gap-1.5 rounded-md border border-input bg-input-bg px-2.5 py-1.5 text-sm shadow-xs with focus ring
ComboboxChipbg-muted text-foreground h-[calc(--spacing(5.5))] px-1.5 text-xs font-medium rounded-sm
ComboboxChipsInputmin-w-16 flex-1 outline-none

States

Input States

StateAppearance
DefaultStandard InputGroup styling — border-input bg-input-bg shadow-xs (InputGroup container provides the bg-input-bg surface; the inner InputGroupInput is bg-transparent)
Focusborder-ring ring-[3px] ring-ring/50 (inherited from InputGroup)
Disabledopacity-50 pointer-events-none on input
Invalidborder-destructive ring-destructive/20

Item States

StateBackgroundText ColorIndicator
Defaulttransparentinheritedhidden
Highlightedbg-accenttext-accent-foreground
SelectedCheckIcon visible (size-4)
Disabledopacity-50 pointer-events-none

Content Animation

TransitionClasses
Openanimate-in fade-in-0 zoom-in-95
Closeanimate-out fade-out-0 zoom-out-95
Slide (per side)slide-in-from-top-2 / slide-in-from-bottom-2 / slide-in-from-left-2 / slide-in-from-right-2
Durationduration-100

Chips States

StateAppearance
Defaultborder-input bg-input-bg shadow-xs
Focus-withinborder-ring ring-[3px] ring-ring/50
Invalidborder-destructive ring-destructive/20 ring-[3px]
Chip disabledpointer-events-none cursor-not-allowed opacity-50

Icons

IconUse ForSize
ChevronDownIconTrigger indicator inside ComboboxInputsize-4 text-muted-foreground
CheckIconSelected item indicatorsize-4 (coarse: size-5)
XIconClear button and chip remove buttoninherited from Button size-icon-xs

Common Patterns

Basic Combobox

const frameworks = [
  { value: "react", label: "React" },
  { value: "vue", label: "Vue" },
  { value: "angular", label: "Angular" },
  { value: "svelte", label: "Svelte" },
]

<Combobox>
  <ComboboxInput placeholder="Select framework..." />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      {frameworks.map((framework) => (
        <ComboboxItem key={framework.value} value={framework.value}>
          {framework.label}
        </ComboboxItem>
      ))}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Combobox with Clear Button

<Combobox>
  <ComboboxInput
    placeholder="Select framework..."
    showTrigger={true}
    showClear={true}
  />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      {frameworks.map((framework) => (
        <ComboboxItem key={framework.value} value={framework.value}>
          {framework.label}
        </ComboboxItem>
      ))}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Multi-Select with Chips

function MultiSelectCombobox() {
  const anchor = useComboboxAnchor()

  return (
    <Combobox multiple>
      <ComboboxChips ref={anchor}>
        {(item) => (
          <ComboboxChip key={item.value} value={item.value}>
            {item.label}
          </ComboboxChip>
        )}
        <ComboboxChipsInput placeholder="Select items..." />
      </ComboboxChips>
      <ComboboxContent anchor={anchor}>
        <ComboboxList>
          <ComboboxEmpty>No results.</ComboboxEmpty>
          {items.map((item) => (
            <ComboboxItem key={item.value} value={item.value}>
              {item.label}
            </ComboboxItem>
          ))}
        </ComboboxList>
      </ComboboxContent>
    </Combobox>
  )
}

Grouped Items

<Combobox>
  <ComboboxInput placeholder="Search..." />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxEmpty>No results found.</ComboboxEmpty>
      <ComboboxGroup>
        <ComboboxLabel>Frameworks</ComboboxLabel>
        <ComboboxItem value="react">React</ComboboxItem>
        <ComboboxItem value="vue">Vue</ComboboxItem>
      </ComboboxGroup>
      <ComboboxSeparator />
      <ComboboxGroup>
        <ComboboxLabel>Build Tools</ComboboxLabel>
        <ComboboxItem value="vite">Vite</ComboboxItem>
        <ComboboxItem value="webpack">Webpack</ComboboxItem>
      </ComboboxGroup>
    </ComboboxList>
  </ComboboxContent>
</Combobox>

With Searchable Content in Popup

<Combobox>
  <ComboboxTrigger>
    <ComboboxValue placeholder="Select item..." />
  </ComboboxTrigger>
  <ComboboxContent>
    <ComboboxInput placeholder="Filter..." />
    <ComboboxList>
      <ComboboxEmpty>Nothing found.</ComboboxEmpty>
      {items.map((item) => (
        <ComboboxItem key={item.value} value={item.value}>
          {item.label}
        </ComboboxItem>
      ))}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Controlled Value

const [value, setValue] = useState("")

<Combobox value={value} onValueChange={setValue}>
  <ComboboxInput placeholder="Select framework..." />
  <ComboboxContent>
    <ComboboxList>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      {frameworks.map((framework) => (
        <ComboboxItem key={framework.value} value={framework.value}>
          {framework.label}
        </ComboboxItem>
      ))}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Accessibility

  • Built on @base-ui/react Combobox — implements WAI-ARIA combobox pattern natively
  • Arrow Up/Down navigates highlighted items
  • Enter selects the currently highlighted item
  • Escape closes the popup
  • Type-ahead filtering via the input
  • aria-expanded, aria-activedescendant, and aria-selected managed automatically
  • Supports pointer-coarse media query for larger touch targets (labels: text-sm, check icon: size-5)

Gotchas

ProblemSolution
Popup not anchored to chipsPass anchor ref from useComboboxAnchor() to both ComboboxChips ref and ComboboxContent anchor
Clear button not visibleSet showClear={true} on ComboboxInput — hidden by default
Trigger button hidden when clear is activeBy design — trigger hides via group-has-data-[slot=combobox-clear]/input-group:hidden when clear is present
Empty state not showingWrap empty message in <ComboboxEmpty> inside <ComboboxList> — it uses group-data-empty to toggle visibility
Popup width too narrowContent uses w-(--anchor-width) with a minimum of min-w-[calc(var(--anchor-width)+--spacing(7))]
Chip remove not workingEnsure showRemove={true} (default) on ComboboxChip
Popup not positioned correctlyCheck side, sideOffset, align, alignOffset props on ComboboxContent
Input in popup not styledPopup applies *:data-[slot=input-group]:bg-input/30 and *:data-[slot=input-group]:border-input/30 for nested inputs

See Also


Last updated: February 24, 2026