ComponentsForms & inputs

Combobox

Searchable selection — single or multiple, static or server-backed, optionally creatable: country, assignee, labels, category. Search folds locale characters (“istanbul” finds “İstanbul”, “cagri” finds “Çağrı”) and highlights matches by weight. Multi-select shows removable chips; async search is debounced, abortable and paged through useAsyncList.

cmdk + Radix Popover components/combobox.tsx, lib/use-async-list.ts, lib/text.ts, components/popover.tsx, components/badge.tsxtheme · material · accent · density · font

Used for tax on your invoices.

Installation

$ pnpm add @vamos-ui/react

The whole kit as one package — get updates by bumping the version. Add the styles to globals.css: @import "@vamos-ui/react/styles.css";

Usage

import { Combobox, Field } from "@vamos-ui/react";

<Field label="Country">
  <Combobox name="country" items={countries} clearable />
</Field>

<Combobox multiple items={labels} defaultValue={["bug"]} maxVisibleChips={3} />

<Combobox
  loadItems={(q, signal, cursor) => fetch(`/api/people?q=${q}&after=${cursor ?? ""}`, { signal }).then((r) => r.json())}
  getValue={(p) => p.id}
  getLabel={(p) => p.name}
/>

WAI-ARIA combobox + listbox: the search box owns aria-activedescendant; ↑/↓ move, Enter selects, Esc closes and returns focus to the trigger.

The result count is announced politely while you type. Async loading sets aria-busy on the list; errors show a retry button.

Matches are emphasised with weight (font-semibold), not the accent colour, so they stay readable on glass.

Examples

Multiple with chips (helpdesk)

The list stays open while you pick. Chips are Badges with a remove button; ←/→ move between chips, Backspace or Delete removes one, and maxVisibleChips collapses the rest into +N.

  • Bug
  • Billing
  • Urgent
  • 1 more selected

Backspace in the search box removes the last label.

Server search with paging

loadItems is debounced (200 ms), aborts the superseded request and caches per query. Return { items, cursor } and the list loads the next page on scroll or via “Show more results”. selectedItems supplies labels for initial values.

Results come from the server, 8 at a time — scroll for more.

Create on the fly (CMS)

onCreate shows “Create “…”” for unknown text. Return the new item (or a promise of it) and it is selected.

Type a new name to create a category.

Avatar and email rows (CRM)

renderItem draws the row; the highlighted label comes in as state.highlight. keywords make email and company searchable.

Search by name, email or company.

Inline list (permissions)

mode="inline" renders the search and list in place — for settings panels and side sheets. Named by the surrounding Field.

3 selected

API

Combobox

PropTypeDefault
items
Static items, filtered client-side (prefix > word start > contains, locale-folded).
T[]—
loadItems
Server search. Debounced, aborted when superseded, cached per query, paged with cursor.
(query, signal, cursor?) => Promise<T[] | { items, cursor }>—
debounce
ms before loadItems runs after typing.
number200
multiple
Select several values; the trigger shows chips.
booleanfalse
value / defaultValue
Selected value(s). Controlled when value is not undefined.
string | null · string[] (multiple)—
onValueChange
Called with the value(s) and the matching item(s).
(value, item) => void · (values, items) => void—
getValue / getLabel
Read any item shape.
(item: T) => stringitem.value / item.label
getKeywords / getDisabled / getGroup
Extra search text, disabled rows, group headings.
(item: T) => …item.keywords / .disabled / .group
renderItem
Custom row content; the check indicator is added for you.
(item, { selected, query, highlight }) => ReactNode—
onCreate
Adds a “Create “…”” row. Return the item to select it.
(query) => T | void | Promise<T | void>—
selectedItems
Items for initial values not in the current list (async).
T[]—
mode
Floating list under a trigger, or search + list in place.
"popover" | "inline""popover"
clearable
× button to clear the selection.
booleanfalse
maxVisibleChips
multiple: collapse further chips into +N (announced as “N more selected”).
number—
closeOnSelect
Close the popover after picking.
booleantrue single · false multiple
placeholder / searchPlaceholder
Trigger text with no selection · search box placeholder (also its accessible name).
string—
emptyMessage
Shown when nothing matches.
ReactNode"No results"
size / variant
Same box as Input and Select.
"sm" | "md" | "lg" · "default" | "ghost" | "pill""md" · "default"
loading
Static items are still loading (spinner + aria-busy).
boolean—
name
Hidden input(s); multiple submits one entry per value (formData.getAll).
string—
disabled / readOnly / invalid / required
Also inherited from Field.
boolean—
open / defaultOpen / onOpenChange
Control the popover.
boolean / (open) => void—
className / contentClassName / listClassName
Root box · popover panel · list (max height).
string—
labels
search, empty, create, results, error, retry, loadMore, clear, remove, more…
Partial<ComboboxLabels>—

useAsyncList

PropTypeDefault
load
The loader. Absent = idle.
(query, signal, cursor?) => Promise<T[] | { items, cursor }>—
query
Each change starts a debounced first-page request; the previous one is aborted.
string—
debounce / enabled / cache / minQueryLength
Timing, pause (e.g. while closed), per-query cache, minimum query length.
number / boolean / boolean / number200 / true / true / 0
→ returns
State for a custom list (Record picker, User picker, Tag input suggestions).
{ items, loading, loadingMore, error, hasMore, loadedQuery, loadMore(), reload() }—