Record Picker
Pick one or several records of the kinds the app passes in — search, kind filter, recents and refused rows — in a popover or a dialog.
- Status
- Since
0.25.89- Accessibility pattern
- combobox search, grouped listbox, disabled rows with reasons
Last updated
Install
Configure registry credentials first using the Quickstart. Before copying this item or any transitive registry dependency, verify the signed manifest and save the exact bytes for Record Picker before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify record-pickerInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/record-pickerRun the exact offline --post-write command printed by the preflight, using its saved item path and independently retained --expected-integrity digest. Use the saved preflight for every transitive registry dependency too; stop on any mismatch before using the components.
The same command installs the registry items it composes: @vegastack/button, @vegastack/command, @vegastack/dialog, @vegastack/use-async-search.
Usage
import { RecordPicker } from "@/components/ui/record-picker";
<PopoverContent size="lg" className="p-0">
<RecordPicker
kinds={[
{ id: "page", noun: "Pages", icon: <FileText /> },
{ id: "table", noun: "Tables", icon: <Table2 /> },
]}
search={(q, { kind, cursor, signal }) =>
api.search({ q, kind, cursor }, { signal })
}
recents={recent}
onPick={(item) => link(item)}
/>
</PopoverContent>;Scope
One way to pick records, whatever their kind: a relation field, a block's source, a "Link to…"
dialog. The picker knows no kinds — the app passes them with their nouns and icons, and its own
search loader (debounced, abortable and paged through useAsyncSearch). People have
PeoplePicker; a place to move something has DestinationPicker.
Anatomy
Examples
In a popover
Type to search; the kind filter narrows it ("All" by default); recents show while the box is
empty; results group by kind. A record with a disabledReason shows why and can't be picked.
In a dialog, picking several
RecordPickerDialog with multiple: the dialog starts from value (the records picked before),
rows toggle with a check, and Done reports the whole set through onConfirm (onValueChange
follows each toggle). value holds records, not keys, so an earlier pick survives a search that
no longer shows it. While a new search is on its way, Enter can't pick a row from the last one.
Link records
Link records
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
kinds* | readonly RecordPickerKind[] | — | The kinds on offer, in filter order. |
search* | RecordPickerSearch | — | Loads one page of results for a query and kind. |
allLabel | string | "All" | |
className | string | — | Classes on the command root. |
defaultKind | string | null | null | The kind filter to start on; null is all kinds. |
emptyLabel | string | "No results." | |
loadMoreLabel | string | "Load more" | |
multiple | boolean | false | Pick several: rows toggle and onValueChange reports the whole set; otherwise a pick calls
onPick and the host closes the picker. |
onPick | ((item: RecordPickerItem) => void) | — | Single: the record picked. |
onValueChange | ((items: RecordPickerItem[]) => void) | — | Multiple: the new set of picked records. |
pickedLabel | string | "picked" | Read after a picked row's title with multiple. |
placeholder | string | "Search…" | |
recentLabel | string | "Recent" | |
recents | readonly RecordPickerItem[] | — | The reader's recent records, shown while the search box is empty. |
retryLabel | string | "Try again" | |
value | readonly RecordPickerItem[] | — | Multiple: the picked records, shown checked. Records, not keys, so a pick made earlier survives whatever the current search shows. |
Data attributes and CSS variables on RecordPicker
| Attribute | Values |
|---|---|
data-checked | "" |
data-slot | "record-picker" | "record-picker-footer" | "record-picker-item" | "record-picker-kinds" |
RecordPickerDialog
| Prop | Type | Default | Description |
|---|---|---|---|
kinds* | readonly RecordPickerKind[] | — | The kinds on offer, in filter order. |
onOpenChange* | (open: boolean) => void | — | Called when it opens or closes. |
open* | boolean | — | Whether the dialog is open. |
search* | RecordPickerSearch | — | Loads one page of results for a query and kind. |
title* | string | — | The dialog's title ("Link a record"). |
allLabel | string | "All" | |
className | string | — | Classes on the command root. |
confirmLabel | string | "Done" | Multiple: the confirm button's label; the dialog then reports the set with onConfirm. |
defaultKind | string | null | null | The kind filter to start on; null is all kinds. |
description | string | — | What the dialog is for, read with the title. |
emptyLabel | string | "No results." | |
loadMoreLabel | string | "Load more" | |
multiple | boolean | false | Pick several: rows toggle and onValueChange reports the whole set; otherwise a pick calls
onPick and the host closes the picker. |
onConfirm | ((items: RecordPickerItem[]) => void) | — | Multiple: called with the picked set when the reader confirms. The dialog starts from value
each time it opens. |
onPick | ((item: RecordPickerItem) => void) | — | Single: the record picked. |
onValueChange | ((items: RecordPickerItem[]) => void) | — | Multiple: the new set of picked records. |
pickedLabel | string | "picked" | Read after a picked row's title with multiple. |
placeholder | string | "Search…" | |
recentLabel | string | "Recent" | |
recents | readonly RecordPickerItem[] | — | The reader's recent records, shown while the search box is empty. |
retryLabel | string | "Try again" | |
value | readonly RecordPickerItem[] | — | Multiple: the picked records, shown checked. Records, not keys, so a pick made earlier survives whatever the current search shows. |
Accessibility
| Key | Does |
|---|---|
| Type | Searches the app's records |
| ↑ / ↓ | Moves through the results |
| Enter | Picks (or toggles, with multiple) |
| Esc | Closes the popover or dialog |
- The search box drives a grouped listbox; a refused row is
aria-disabledand reads its reason. - Loading, errors and "Load more" sit outside the listbox, in a status row.
| Contract | States tested |
|---|---|
| Behaviour | default, loading, error, empty, disabled-item, multiple |
| Accessibility | labeled |
| Visual | default, with-icon |
Do / Don't
People Picker
The one picker for people and teams — single or multiple, with avatar rows, "(you)", team sizes, skeleton loading and footer actions.
Destination Picker
Choose where something goes — spaces at the top of a folders-only tree, recent places, a folder search, and destinations refused with a reason.