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
| Part | Tailwind |
|---|---|
| ComboboxInput (wrapper) | w-auto (InputGroup) — input renders via InputGroupInput |
| Trigger button | size-icon-xs variant-ghost via InputGroupButton, icon size-4 text-muted-foreground |
| Clear button | size-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))] |
| ComboboxList | scroll-py-1 overflow-y-auto p-1 data-empty:p-0 with max-height clamped to available space |
| ComboboxItem | py-1.5 pr-8 pl-2 gap-2 rounded-sm text-sm with CheckIcon indicator at absolute right-2 |
| ComboboxGroup | pass-through (no default styles) |
| ComboboxLabel | px-2 py-1.5 text-xs text-muted-foreground (coarse: px-3 py-2 text-sm) |
| ComboboxEmpty | text-muted-foreground text-sm text-center py-2 — hidden until data-empty on content |
| ComboboxSeparator | bg-border -mx-1 my-1 h-px |
| ComboboxChips | min-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 |
| ComboboxChip | bg-muted text-foreground h-[calc(--spacing(5.5))] px-1.5 text-xs font-medium rounded-sm |
| ComboboxChipsInput | min-w-16 flex-1 outline-none |
States
Input States
| State | Appearance |
|---|---|
| Default | Standard InputGroup styling — border-input bg-input-bg shadow-xs (InputGroup container provides the bg-input-bg surface; the inner InputGroupInput is bg-transparent) |
| Focus | border-ring ring-[3px] ring-ring/50 (inherited from InputGroup) |
| Disabled | opacity-50 pointer-events-none on input |
| Invalid | border-destructive ring-destructive/20 |
Item States
| State | Background | Text Color | Indicator |
|---|---|---|---|
| Default | transparent | inherited | hidden |
| Highlighted | bg-accent | text-accent-foreground | — |
| Selected | — | — | CheckIcon visible (size-4) |
| Disabled | — | — | opacity-50 pointer-events-none |
Content Animation
| Transition | Classes |
|---|---|
| Open | animate-in fade-in-0 zoom-in-95 |
| Close | animate-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 |
| Duration | duration-100 |
Chips States
| State | Appearance |
|---|---|
| Default | border-input bg-input-bg shadow-xs |
| Focus-within | border-ring ring-[3px] ring-ring/50 |
| Invalid | border-destructive ring-destructive/20 ring-[3px] |
| Chip disabled | pointer-events-none cursor-not-allowed opacity-50 |
Icons
| Icon | Use For | Size |
|---|---|---|
ChevronDownIcon | Trigger indicator inside ComboboxInput | size-4 text-muted-foreground |
CheckIcon | Selected item indicator | size-4 (coarse: size-5) |
XIcon | Clear button and chip remove button | inherited 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/reactCombobox — 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, andaria-selectedmanaged automatically- Supports
pointer-coarsemedia query for larger touch targets (labels:text-sm, check icon:size-5)
Gotchas
| Problem | Solution |
|---|---|
| Popup not anchored to chips | Pass anchor ref from useComboboxAnchor() to both ComboboxChips ref and ComboboxContent anchor |
| Clear button not visible | Set showClear={true} on ComboboxInput — hidden by default |
| Trigger button hidden when clear is active | By design — trigger hides via group-has-data-[slot=combobox-clear]/input-group:hidden when clear is present |
| Empty state not showing | Wrap empty message in <ComboboxEmpty> inside <ComboboxList> — it uses group-data-empty to toggle visibility |
| Popup width too narrow | Content uses w-(--anchor-width) with a minimum of min-w-[calc(var(--anchor-width)+--spacing(7))] |
| Chip remove not working | Ensure showRemove={true} (default) on ComboboxChip |
| Popup not positioned correctly | Check side, sideOffset, align, alignOffset props on ComboboxContent |
| Input in popup not styled | Popup applies *:data-[slot=input-group]:bg-input/30 and *:data-[slot=input-group]:border-input/30 for nested inputs |
See Also
- Related Components: Select (without search), Command (command palette), Form (form integration)
- Internal Dependencies: InputGroup (used by ComboboxInput), Button (used by clear/chip remove)
- Accessibility: Forms Accessibility — Combobox ARIA patterns
- Patterns: Component Match — Select vs Combobox
Last updated: February 24, 2026