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

PartRole
ComboBox.RootOwns selectionMode, the value (selectedKey in single mode; value / defaultValue / onChange in multiple mode), disabledKeys, defaultFilter.
ComboBox.LabelThe field's visible label.
ComboBox.ControlThe field box: the text input and chevron button, plus the selected tags in multiple mode. Takes placeholder.
ComboBox.ContentThe popover listbox. Takes items + a render function, or static children; React Aria filters them against the input.
ComboBox.ItemOne 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.

Colorado
Texas
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.

Swimming pool

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

Free
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.