Skip to content
Component installs need the registry setup
VegaStack Design

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
preview
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-picker

Install with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.

pnpm dlx shadcn@4.21.0 add @vegastack/record-picker

Run 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

RecordPicker — data-slot="record-picker" | "record-picker-footer" | "record-picker-item" | "record-picker-kinds"
RecordPickerDialog

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

PropTypeDefaultDescription
kinds*readonly RecordPickerKind[]—The kinds on offer, in filter order.
search*RecordPickerSearch—Loads one page of results for a query and kind.
allLabelstring"All"
classNamestring—Classes on the command root.
defaultKindstring | nullnullThe kind filter to start on; null is all kinds.
emptyLabelstring"No results."
loadMoreLabelstring"Load more"
multiplebooleanfalsePick 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.
pickedLabelstring"picked"Read after a picked row's title with multiple.
placeholderstring"Search…"
recentLabelstring"Recent"
recentsreadonly RecordPickerItem[]—The reader's recent records, shown while the search box is empty.
retryLabelstring"Try again"
valuereadonly 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

AttributeValues
data-checked""
data-slot"record-picker" | "record-picker-footer" | "record-picker-item" | "record-picker-kinds"

RecordPickerDialog

PropTypeDefaultDescription
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").
allLabelstring"All"
classNamestring—Classes on the command root.
confirmLabelstring"Done"Multiple: the confirm button's label; the dialog then reports the set with onConfirm.
defaultKindstring | nullnullThe kind filter to start on; null is all kinds.
descriptionstring—What the dialog is for, read with the title.
emptyLabelstring"No results."
loadMoreLabelstring"Load more"
multiplebooleanfalsePick 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.
pickedLabelstring"picked"Read after a picked row's title with multiple.
placeholderstring"Search…"
recentLabelstring"Recent"
recentsreadonly RecordPickerItem[]—The reader's recent records, shown while the search box is empty.
retryLabelstring"Try again"
valuereadonly RecordPickerItem[]—Multiple: the picked records, shown checked. Records, not keys, so a pick made earlier survives whatever the current search shows.

Accessibility

KeyDoes
TypeSearches the app's records
↑ / ↓Moves through the results
EnterPicks (or toggles, with multiple)
EscCloses the popover or dialog
  • The search box drives a grouped listbox; a refused row is aria-disabled and reads its reason.
  • Loading, errors and "Load more" sit outside the listbox, in a status row.
ContractStates tested
Behaviourdefault, loading, error, empty, disabled-item, multiple
Accessibilitylabeled
Visualdefault, with-icon

Do / Don't

Do
Use one RecordPicker for every 'pick a record' job, passing the kinds and the app's search.
Don't
Build a picker dialog per record kind, or filter a full list of records on the client.

On this page