Combobox
A text field that filters a list of options as you type.
A text field that filters a list of options as you type. The difference from Select is where the typing happens: a Select is a button that opens a list, and its optional filter lives inside that list once it is open. A Combobox is the field itself — you are already typing when the options appear, which is what you want when the value is something you know the name of rather than something you expect to recognise by scrolling.
Installation
Combobox ships with the library — no separate install.
import { Combobox, Label } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add comboboxUsage
<Combobox
value={framework}
onValueChange={setFramework}
placeholder="Search frameworks"
clearable
>
<Combobox.Item value="expo" label="Expo" />
<Combobox.Item value="next" label="Next.js" />
</Combobox>Composition
<Combobox>
<Combobox.Item value="…" label="…" />
<Combobox.Group label="…">
<Combobox.Item value="…" label="…" />
</Combobox.Group>
</Combobox>Combobox.Item— One option.valueidentifies it,labelis what is shown and what the filter matches on,descriptionadds a second line,startdraws something before the label, anddisabledkeeps it listed but unselectable.Combobox.Group— A titled run of options. Presentational only — a grouped Combobox reports the same values a flat one would.
Examples
Filter as you type
The list narrows on every keystroke; clearable offers a ✕ that empties the field.
const [framework, setFramework] = useState<string>();
<Combobox
value={framework}
onValueChange={setFramework}
placeholder="Search frameworks"
clearable
>
{frameworks.map((f) => (
<Combobox.Item key={f.value} value={f.value} label={f.label} />
))}
</Combobox>Grouped options
A group is dropped when the query empties it, rather than leaving its heading behind.
<Combobox value={framework} onValueChange={setFramework} openOnFocus>
<Combobox.Group label="Native">
<Combobox.Item value="expo" label="Expo" />
<Combobox.Item value="react-native" label="React Native" />
</Combobox.Group>
<Combobox.Group label="Web">
<Combobox.Item value="next" label="Next.js" />
<Combobox.Item value="remix" label="Remix" />
</Combobox.Group>
</Combobox>Several at once
Each pick becomes a removable chip in front of the input, and the list stays open.
const [picked, setPicked] = useState<string[]>(['expo']);
<Combobox
mode="multiple"
value={picked}
onValueChange={setPicked}
placeholder="Add a framework"
clearable
>
{frameworks.map((f) => (
<Combobox.Item key={f.value} value={f.value} label={f.label} />
))}
</Combobox>A tag field
allowCustomValue keeps whatever was typed, so the list only suggests.
<Combobox
mode="multiple"
value={tags}
onValueChange={setTags}
allowCustomValue
placeholder="Add a tag"
>
<Combobox.Item value="design" label="design" />
<Combobox.Item value="engineering" label="engineering" />
</Combobox>Fetched for the query
The server does the matching, so the field does not filter again — it only shows what came back.
const [query, setQuery] = useState('');
const { results, loading } = useCitySearch(query);
<Combobox
value={city}
onValueChange={setCity}
inputValue={query}
onInputValueChange={setQuery}
filter={false}
loading={loading}
loadingMessage="Looking up cities"
emptyMessage="No city by that name"
placeholder="Search cities"
>
{results.map((c) => (
<Combobox.Item key={c.value} value={c.value} label={c.label} />
))}
</Combobox>Expanding in place
inline pushes what is below the field down instead of covering it.
<Combobox
presentation="inline"
value={framework}
onValueChange={setFramework}
placeholder="Search frameworks"
>
{/* …items… */}
</Combobox>Variants
itemDisabled
true
<Combobox itemDisabled="true">…</Combobox>presentation
overlay(default)inline
<Combobox presentation="overlay">…</Combobox>
<Combobox presentation="inline">…</Combobox>API Reference
Combobox.Item
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | |
label | string | — | |
disabled | boolean | — | Shows the option but refuses it. Kept in the list rather than dropped from it, because an option that vanishes reads as one that never existed. |
start | ReactNode | — | Anything to draw before the label — an avatar, a flag, a status dot. |
description | string | — | A second line under the label, for what the label alone cannot say. |
Combobox.Group
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Heading over the run of options. Announced as a header, so a screen reader reaching the group is told what it is before walking into it. |
className | string | — | Extra classes for the group wrapper. |
labelClassName | string | — | Extra classes for the heading. |
Combobox
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
mode | Mode | — | One value or several. multiple draws the chosen options as removable chips in front of the input and keeps the list open between picks. |
value | ComboboxSelection[Mode] | — | Controlled selection. Its shape follows mode. |
defaultValue | ComboboxSelection[Mode] | — | Starting selection when uncontrolled. |
onValueChange | (value: ComboboxSelection[Mode]) => void | — | |
inputValue | string | — | Controlled query — the text actually in the field. Pair it with onInputValueChange when the options are fetched for it. |
defaultInputValue | string | '' | Starting query when uncontrolled. |
onInputValueChange | (value: string) => void | — | |
placeholder | string | 'Search' | |
disabled | boolean | false | |
presentation | ComboboxPresentation | 'overlay' | Where the options appear. |
filter | boolean | ((option: ComboboxItemProps, query: string) => boolean) | true | Narrow the options to the query here. true matches case-insensitively on any part of an option's label; pass a function to match on something else — a description, an alias list, an initialism. Pass false when a server is doing the matching: the options you render are then shown exactly as given, since a second filter over results the field cannot see the query behind would only remove correct answers. |
allowCustomValue | boolean | false | Let the typed text become the value when it matches no option, committed on submit. Turns the list into a set of suggestions rather than the set of legal answers — which is what a tag field is. |
loading | boolean | false | Show a spinner in place of the list. For options still being fetched. |
emptyMessage | string | 'No matches' | Shown in place of the list when nothing matches. |
loadingMessage | string | 'Searching' | Shown in place of the list while loading. |
clearable | boolean | false | Offer a ✕ that clears the query and the selection. |
openOnFocus | boolean | false | Open the list as soon as the field takes focus, before anything is typed. |
onOpenChange | (open: boolean) => void | — | Called when the list opens or closes. |
contentWidth | 'field' | 'content' | number | 'field' | Width of the floating list. field matches the field, content sizes to the longest option, or pass a pixel value. overlay only. |
offset | number | 8 | Gap between the field and the floating list. overlay only. |
listClassName | string | — | Extra classes for the list surface. |
accessibilityLabel | string | — | Accessible name for the field. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
Choosing a presentation
overlay (the default) floats the list through a portal, anchored under the field and flipped above it when the keyboard leaves no room below. Nothing else on the screen moves. contentWidth and offset apply here only.
inline expands the list in normal layout flow instead, pushing what is below it down. Reach for it in a form where nothing should be covered.
There is deliberately no sheet presentation. A sheet takes the bottom of the screen, which is where the keyboard already is, and the field you are typing into would end up behind one or the other. Select can offer a sheet because its trigger stops mattering once the list is open; a Combobox's never does.
One value or several
mode="multiple" draws the chosen options as removable chips in front of the input and keeps the list open between picks, so several can be taken in a row. The value is an array there and a single string otherwise — the type follows mode, so nothing has to be cast.
Picking an already-chosen option removes it, which is the only way to undo a pick without reaching for its chip. Backspace on an empty field takes the last chip back: the same reflex that deletes a character, extended to the thing in front of the cursor when there is no character left to delete.
Filtering, and turning it off
Filtering happens in the component by default, matching case-insensitively on any part of an option's label. Pass a function to filter to match on something else — a description, an alias list, an initialism.
Pass filter={false} when the options come from a server that is doing the matching itself. The field then renders exactly what it was given, because a second filter over results it cannot see the query behind would only remove correct answers. Pair it with the controlled inputValue / onInputValueChange so the query can drive the request, and with loading so the list says it is working rather than saying there are no matches.
Values it does not know about
allowCustomValue lets the typed text become the value when it matches no option, committed on return. That turns the list into a set of suggestions rather than the set of legal answers, which is what a tag field is.
Accessibility
The field is announced as a combobox that owns an expandable list, so the chips and the input read as parts of one control rather than as loose siblings, and the open state is announced on a role where it means something. The floating list is a modal layer: it takes the Android back button and hides the page behind it from the accessibility tree, so a screen reader cannot walk out of an open list into content it is covering.