ComboBox
Filterable picker built on React Aria's ComboBox. Typing in the field filters
the options, so it suits lists too long to scan — states, markets, categories.
With selectionMode="multiple" the selection renders as removable tags inside
the field, ahead of the input, the way a chip input works.
Reach for the plain Select when the list is short enough to scan without
typing.
import { ComboBox } from "@pockets-ui/components"Anatomy
| Part | Role |
|---|---|
ComboBox.Root | Owns selectionMode, the value (selectedKey in single mode; value / defaultValue / onChange in multiple mode), disabledKeys, defaultFilter. |
ComboBox.Label | The field's visible label. |
ComboBox.Control | The field box: the text input and chevron button, plus the selected tags in multiple mode. Takes placeholder. |
ComboBox.Content | The popover listbox. Takes items + a render function, or static children; React Aria filters them against the input. |
ComboBox.Item | One option. Requires id when declared statically — that's the selection key. |
Basic
Single selection: typing filters the list, picking fills the input and closes the popover. The chevron opens the full list.
Code
import { ComboBox } from "@pockets-ui/components"
const CATEGORIES = [
"News",
"Travel",
"Shopping",
"Business",
"Entertainment",
"Food",
"Technology",
"Health",
"Science",
].map((name) => ({ id: name.toLowerCase(), name }))
export default function ComboBoxBasic() {
return (
<ComboBox.Root>
<ComboBox.Label>Category</ComboBox.Label>
<ComboBox.Control placeholder="Search categories" />
<ComboBox.Content items={CATEGORIES}>
{(category) => <ComboBox.Item>{category.name}</ComboBox.Item>}
</ComboBox.Content>
</ComboBox.Root>
)
}Multiple selection
selectionMode="multiple" on the root renders the selection as tags inside the
field, before the input. The popover stays open while picking and the input
clears after each pick. Remove a tag with its button, or press Backspace in the
empty input to drop the last one. The value is an array of keys.
Code
import { ComboBox } from "@pockets-ui/components"
const STATES = [
{ id: "AZ", name: "Arizona" },
{ id: "CA", name: "California" },
{ id: "CO", name: "Colorado" },
{ id: "FL", name: "Florida" },
{ id: "GA", name: "Georgia" },
{ id: "IN", name: "Indiana" },
{ id: "NC", name: "North Carolina" },
{ id: "OH", name: "Ohio" },
{ id: "TN", name: "Tennessee" },
{ id: "TX", name: "Texas" },
]
export default function ComboBoxMultiple() {
return (
// selectionMode="multiple" renders the selection as tags inside the field,
// ahead of the input. The popover stays open while picking; remove a tag
// (or press Backspace in the empty input) to deselect.
<ComboBox.Root selectionMode="multiple" defaultValue={["CO", "TX"]}>
<ComboBox.Label>Markets</ComboBox.Label>
<ComboBox.Control
placeholder="Add markets"
tagsLabel="Selected markets"
/>
<ComboBox.Content items={STATES}>
{(state) => <ComboBox.Item>{state.name}</ComboBox.Item>}
</ComboBox.Content>
</ComboBox.Root>
)
}Controlled
value with onChange keeps the selection in your own state. emptyState on
Content customises the message shown when the search matches nothing.
Selected: pool
Code
import { ComboBox, type Key } from "@pockets-ui/components"
import { useState } from "react"
const AMENITIES = [
{ id: "laundry", name: "Laundry" },
{ id: "fitness", name: "Fitness center" },
{ id: "parking", name: "Parking" },
{ id: "pool", name: "Swimming pool" },
{ id: "breakfast", name: "Breakfast" },
]
export default function ComboBoxControlled() {
const [value, setValue] = useState<Key[]>(["pool"])
return (
<div
style={{
display: "flex",
flexDirection: "column",
gap: 12,
width: "100%",
}}
>
<ComboBox.Root selectionMode="multiple" value={value} onChange={setValue}>
<ComboBox.Label>Amenities</ComboBox.Label>
<ComboBox.Control
placeholder="Add amenities"
tagsLabel="Selected amenities"
/>
<ComboBox.Content items={AMENITIES} emptyState="No amenities match">
{(amenity) => <ComboBox.Item>{amenity.name}</ComboBox.Item>}
</ComboBox.Content>
</ComboBox.Root>
<p style={{ margin: 0, fontSize: 14 }}>
Selected: {value.length ? value.join(", ") : "none"}
</p>
</div>
)
}Disabled
Code
import { ComboBox } from "@pockets-ui/components"
export default function ComboBoxDisabled() {
return (
<ComboBox.Root selectionMode="multiple" defaultValue={["free"]} isDisabled>
<ComboBox.Label>Plan</ComboBox.Label>
<ComboBox.Control placeholder="Unavailable" />
<ComboBox.Content>
<ComboBox.Item id="free">Free</ComboBox.Item>
<ComboBox.Item id="pro">Pro</ComboBox.Item>
</ComboBox.Content>
</ComboBox.Root>
)
}Filtering
Options are filtered by their textValue with a case- and accent-insensitive
"contains" match. Pass defaultFilter to Root to change that (for example a
starts-with match), and pass textValue on Item whenever its children aren't
plain text.