Components · Forms

Entity picker

A searchable multi-entity picker. Chosen people & teams render as our own removable Badge tokens in a recessed Surface tray; the Combobox below is driven purely as an add-surface — its value stays null and every pick is promoted into the token list. Shown here with the results list open: entity avatars, one highlighted option, and a create row.

multi-select · token tray + open results · asyncLoad debounced search
  • Ada Lovelace
  • Cyrus Tang
mar

Adoption Day — intake table. Already-selected entities are filtered out of the list; the highlighted row commits on Enter. Tokens carry a roving tabindex — Cyrus Tang is focused (←/→ move, Backspace removes). The dropdown only renders each option's label; the avatars + sublines come from the EntityOption presentation extras.

selection cap · renderToken slot · async loading · empty
maxSelections=3 · field disabled at cap
  • Ada Lovelace
  • Bekah Mendez
  • Dana Reyes
Selection full

3/3 selected — remove one to add another

renderToken · custom token markup
  • Atlanta Organizers
  • SF Bay Team
Add a team…

Chapter chips replace the default Badge via the render slot — Building2 icon, accent-outline pill, matching .

asyncLoad · debounced remote search
distr

Server is the filter — internal matching off, stale responses dropped, emptyText flips to loadingText. 600-row pool, dropdown capped at 50.

empty · resting placeholder
Search teams…

No tokens yet — the tray only renders once a selection exists, so the field sits alone on the surface shell.

items / selected
items is the selectable pool (EntityOption[]); selected is the controlled selection. Already-selected entities are filtered out of the dropdown.
onSelectedChange
Called with the next EntityOption[] on every add or remove — the component holds no selection of its own.
asyncLoad? / debounceMs
(query) => Promise<EntityOption[]>. When set the dropdown shows debounced remote results (stale responses dropped, internal filtering off) and emptyText flips to loadingText while pending. debounceMs defaults to 250.
renderToken? / tokenTone
(item, { onRemove, removeLabel, index }) => ReactNode replaces the default removable Badge. The default token honours item.icon and tokenTone (accent by default).
maxSelections / maxResults
maxSelections caps the selection; the field disables at the cap and a live n/max count is announced. maxResults (default 100) caps options rendered in the dropdown — a cheap guard for large pools.
keyboard / a11y
Tokens use a roving tabindex (←/→/Home/End); Backspace/Delete/Enter/Space removes; Backspace in the empty field drops the last token. Remove controls carry an aria-label; focus is restored after a removal; transitions respect prefers-reduced-motion.

Source · ds/forms/EntityPicker.tsx@phauna/ds/overlays Combobox driven as an add-surface + a self-owned tray of removable DS Badge tokens, both on the Surface primitive (flat shell → inset surface-2 tray).