Combobox
comboboxFilters selectable options through either a trigger popup or an inline input.
Usage
Search within elastic layer (Figure 4 paradigm)
The trigger button displays the selected options. Click to expand the floating layer with the search box.
<Combobox items={fruits}>
<ComboboxTrigger placeholder="Select Fruit" />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>Inline auto-completion
The fields are visible in the input box itself and can be filtered by typing directly. clearable displays the clear button.
<Combobox items={fruits}>
<ComboboxInput placeholder="Search for fruits..." clearable />
<ComboboxContent>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>Search-field form
prefix adds the magnifier and showChevron={false} drops the trailing chevron: the field itself is the search box (typing filters, selecting navigates), so it should not look like a dropdown. Used outside Field, aria-label goes straight on ComboboxInput and lands on the inner input.
<Combobox items={fruits}>
<ComboboxInput
size="sm"
prefix={<SearchIcon />}
showChevron={false}
placeholder="Search fruit"
aria-label="Search fruit"
/>
<ComboboxContent>
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>Default selected value
defaultValue Pass in the option object as an uncontrolled initial value.
<Combobox items={fruits} defaultValue={fruits[2]}>
<ComboboxTrigger placeholder="Select Fruit" />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>Trigger degraded to a status icon
children replaces the default "selected label" block entirely. In a narrow table cell the name already appears elsewhere, so a trigger repeating it reads as two fields - such a slot only has room for an icon. Pass a function to branch on whether anything is selected.
<Combobox items={fruits}>
<ComboboxTrigger
aria-label="Bind a fruit"
showChevron={false}
className="size-8 justify-center px-0"
>
{(value) => (value ? <Link className="size-4" /> : <Plus className="size-4" />)}
</ComboboxTrigger>
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>Disabled and invalid state
disabled is overall gray; invalid trigger is marked red.
<>
<Combobox items={fruits} defaultValue={fruits[0]} disabled>
<ComboboxTrigger placeholder="Select Fruit" />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>
<Combobox items={fruits}>
<ComboboxTrigger placeholder="Select Fruit" invalid />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>
</>Size
size controls the trigger height (sm / md / lg).
<Combobox items={fruits}>
<ComboboxTrigger size="sm" placeholder="Select Fruit" />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => (
<ComboboxItem key={item.value} value={item}>
{item.label}
</ComboboxItem>
)}
</ComboboxContent>
</Combobox>When to use
Use Combobox when users need to filter a substantial option set as they type. It supports a trigger that opens a searchable popup, an inline autocomplete field, and chip-based multiple selection. Use Select for a small fixed list, SecretField to display an existing secret, or Mentions for inline references such as @mentions.
Import
import { Combobox, ComboboxInput, ComboboxTrigger, ComboboxContent, ComboboxItem, ComboboxChips, ComboboxChip } from "@hulianui/ui"Props
Combobox extends Base UI Combobox.Root<ComboboxItemData, Multiple>; the table below lists HulianUI's common additions.
| Name | Type | Default | Description |
|---|---|---|---|
| items | ComboboxItemData[] | - | Options in { value, label } form. The label is displayed while the value identifies the option. |
| value | ComboboxItemData|ComboboxItemData[] | - | Controlled selection; use an array when multiple is true. |
| defaultValue | Same as above | - | Initial selection when uncontrolled. |
| multiple | boolean | false | Changes value and onValueChange to arrays. |
| virtualized | boolean | true once items reaches 100 | Virtualizes the list so only visible options are rendered. Decided from the option count when omitted (see Usage guidelines). |
| creatable | boolean | false | Free-text creation: when the current input has no exact match among the options, a "Use “xxx”" row appears at the top of the list. See below. |
| onCreate | (value: string) => void | - | Fires when the create row is selected, alongside onValueChange rather than instead of it. See below. |
| createLabel | (value: string) => ReactNode | combobox.create from the global locale | Copy of the create row, overridable per instance. Two creatable comboboxes in one app rarely mean the same thing. |
| disabled | boolean | false | Disables the control. |
ComboboxTrigger displays the selected label or placeholder and opens the searchable popup. It extends the native <button> attributes, and remaining attributes land on the button itself.
| Name | Type | Default | Description |
|---|---|---|---|
| size | "sm"|"md"|"lg" | "md" | Trigger size. |
| placeholder | string | - | Text shown when nothing is selected. A button has no native placeholder, so this is a HulianUI prop. |
| invalid | boolean | false | Applies invalid styling when used outside Field. |
| showChevron | boolean | true | Trailing expand chevron. Pass false when the trigger degrades to an icon button. |
| className | string | - | Additional class name for the trigger. |
ComboboxInput provides inline autocomplete: the visible field is also the search input. It extends the native <input> attributes, and remaining attributes land on the inner `<input>` rather than the shell span (see Usage guidelines).
| Name | Type | Default | Description |
|---|---|---|---|
| size | "sm"|"md"|"lg" | "md" | Input size. |
| placeholder | string | - | Input placeholder; a native attribute forwarded to the inner input. |
| invalid | boolean | false | Applies invalid styling. |
| clearable | boolean | false | Shows a clear button when the field has a value. |
| prefix | ReactNode | - | Leading icon slot, matching Input.prefix. Use a magnifier for search fields. |
| showChevron | boolean | true | Trailing expand chevron. Pass false for a search field. |
| className | string | - | Class name for the shell, which carries the field styling and is not forwarded to the input. |
ComboboxContent
| Name | Type | Default | Description |
|---|---|---|---|
| searchPlaceholder | string | - | Adds a search field to the popup when used with ComboboxTrigger. Omit it when ComboboxInput already provides inline search. |
| side | "top"|"bottom" | - | Preferred side of the trigger on which to place the popup. |
| align | "start"|"center"|"end" | - | Popup alignment relative to the trigger. |
| sideOffset | number | - | Distance from the trigger in pixels. |
| onListScroll | UIEventHandler<HTMLDivElement> | - | Called when the option list scrolls. e.currentTarget is the scroll container, which can be inspected to implement load-on-scroll pagination; see RemoteSelect. |
| header | ReactNode | - | Fixed content above the scrolling list, such as a usage hint, group note, or bulk action. Unlike emptyMessage, which only appears when there are no results. |
| footer | ReactNode | - | Fixed content below the scrolling list, such as loading, count, or end-of-results feedback. |
| className | string | - | Additional class name for the popup. |
ComboboxItem
| Name | Type | Default | Description |
|---|---|---|---|
| value * | ComboboxItemData | - | Complete { value, label } option object. |
| disabled | boolean | false | Disables this option. |
| className | string | - | Additional class name for the option. |
ComboboxChips is the multiple-selection field shell and accepts size, invalid, placeholder, className, and children. It extends the native <input> attributes, and remaining attributes land on the inner `<input>` because the chips container is only the visual shell.ComboboxChip renders one selected value and accepts className and children.
Events
Combobox inherits events from Base UI Combobox.Root.
| Event | Type | Description |
|---|---|---|
| onValueChange | (value) => void | Called when the selection changes; value is an array in multiple mode. |
Slots
Combobox
| Slot | Type | Description |
|---|---|---|
| children | ReactNode | Trigger or input plus popup content. |
ComboboxTrigger
| Slot | Type | Description |
|---|---|---|
| children | ReactNode | (value: ComboboxItemData | null) => ReactNode | Custom trigger content replacing the default "selected label ?? placeholder" block. A function branches on the selected value. Omit it for the original behaviour. |
ComboboxContent
| Slot | Type | Description |
|---|---|---|
| children * | (item, index) => ReactNode | Render function invoked once for each filtered option. |
| emptyMessage | ReactNode | Content shown when no option matches the query. |
ComboboxItem
| Slot | Type | Description |
|---|---|---|
| children * | ReactNode | Visible option content. |
ComboboxChips
| Slot | Type | Description |
|---|---|---|
| children | ReactNode | Selected chips and the inline input. |
ComboboxChip
| Slot | Type | Description |
|---|---|---|
| children * | ReactNode | Visible chip content. |
Usage guidelines
- Lists of 100 options or more are virtualized automatically, and you do not pass
virtualizedyourself. Only the visible options stay in the DOM, and row height is estimated at a fixed 32px without per-item measurement. The defaultComboboxItemis exactly 32px tall, so most usage is unaffected. If your render function returns rows of a different height (two lines of text, an avatar, custom padding or font size throughclassName), scrollbar length and item placement drift apart as the list grows. Nothing throws, and short lists never reproduce it; the jump only shows once you scroll past the first screens. Passvirtualized={false}for those rows, or make them 32px tall. - Virtualization also affects tests and scripts that assume every option is in the DOM:
getAllByRole("option")returns only the visible window, anddocument.querySelectorcannot find options you have not scrolled to. Assert totals againstdata-hulian-virtual-counton the list container, or passvirtualized={false}for that test. - Components built on Combobox inherit this: the
searchableskin of Select and the candidate list of RemoteSelect both virtualize once they hold 100 options. ComboboxItemreceives the entire{value,label}object asvalue, not just a string. Passvalue={item}from the render function so Base UI can derive both the label and value.- Set
searchPlaceholderonComboboxContentwhen usingComboboxTriggerto provide search inside the popup. WithComboboxInput, the inline input is already the search field. - Enabling
multiplechangesvalueandonValueChangeto arrays. Controlled state must use the corresponding array type. - Pass
invalidonly when Combobox is used outside Field. Field supplies invalid styling automatically for nested controls. - Remaining native attributes on `ComboboxInput` and `ComboboxChips` land on the inner `<input>`, not on the shell. The
role="combobox", focusability, and form ownership all live on that input, soaria-label,id,name, andonBlurhave no effect on the shell span or the chips container. Outside Field,<ComboboxInput aria-label="Search tasks" />is therefore enough (no wrapping<label>with an.sr-onlyspan), andfield.onBlurfrom a react-hook-formControllercan be passed directly. UseclassNamefor hooks on the outer container.ComboboxTriggerhas no shell, so its remaining attributes land on the button itself. - Styling and
data-invalidsupplied by the component cannot be overridden:restis spread first, following the library-wide contract in section 7 ofdocs/consuming.md. Passingaria-invalid={false}does not clear the styling applied byinvalid. - A search field needs both
prefixandshowChevron={false}. Adding only the magnifier leaves the chevron in place, which still reads as a dropdown. Search inside the popup is a different pattern:searchPlaceholderonComboboxContentrenders its own search field with a built-in magnifier, andComboboxInputis not involved.
Related
SecretField · Listbox · Mentions · InputOTP · Rating · Upload
Playground
<Combobox items={items} defaultValue={items[0]}>
<ComboboxTrigger size="md" placeholder="Select fruit" />
<ComboboxContent searchPlaceholder="Search for fruits...">
{(item) => <ComboboxItem key={item.value} value={item}>{item.label}</ComboboxItem>}
</ComboboxContent>
</Combobox>