Record picker

Multi-select picker over a pool of records. Users can search, select, and create new records inline via a CRUD modal driven by a dynamic field schema. Selections apply live as you toggle them — close the popover when you're done. Full keyboard navigation: ↑/↓ to move the active row, Enter to toggle the active row, and a Clear all button in the footer.

*

Pick existing members or add a new one on the fly.

Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/record-picker-block.json
Usage
Pass a record pool, a field schema, and the controlled selection.
TSImport
import { RecordPicker } from "@/components/blocks/record-picker-block";
import type {
  RecordPickerFieldDef,
  RecordRow,
} from "@/components/blocks/record-picker-block";
TSExample
import * as React from "react";
import { RecordPicker } from "@/components/blocks/record-picker-block";
import type {
  RecordPickerFieldDef,
  RecordRow,
} from "@/components/blocks/record-picker-block";

const fields: RecordPickerFieldDef[] = [
  { key: "name", label: "Full name", type: "text", required: true },
  { key: "email", label: "Email", type: "email", required: true },
  {
    key: "role",
    label: "Role",
    type: "select",
    required: true,
    options: [
      { label: "Designer", value: "Designer" },
      { label: "Engineer", value: "Engineer" },
      { label: "Researcher", value: "Researcher" },
    ],
  },
];

const initial: RecordRow[] = [
  { id: "m-1", name: "Alex Wong",   email: "alex@example.com",   role: "Designer" },
  { id: "m-2", name: "Priya Nair",  email: "priya@example.com",  role: "Engineer" },
];

export function Example() {
  const [records, setRecords] = React.useState<RecordRow[]>(initial);
  const [selection, setSelection] = React.useState<string[]>([initial[0].id]);

  return (
    <RecordPicker
      label="Team members"
      labelKey="name"
      descriptionKey="role"
      records={records}
      onRecordsChange={setRecords}
      value={selection}
      onChange={setSelection}
      fields={fields}
      addLabel="New member"
    />
  );
}
Composition
Anatomy of the record picker.
RecordPicker
├── Trigger (label / description / required indicator)
├── Search list (one item per RecordRow)
├── Inline "Add" CTA → opens edit modal driven by RecordPickerFieldDef[]
└── Selection chips (controlled value: string[] of record ids)

Keyboard (when the popover is open)
├── ArrowUp / ArrowDown   navigate the active row in the filtered list
├── Enter                 toggle the active row's selection (applies live)
└── Esc / outside click   close the popover

Footer
├── "Clear all" (left)    clears the current selection
└── "N selected" (right)  live count of selected records
Members — with inline add
The dropdown search list has a “New member” CTA that opens a modal; new records are added to the pool and auto-selected.
*

Pick existing members or add a new one on the fly.

Projects — different schema
Same component, different field schema and display keys — everything is data-driven.
Locked pool — no inline add
Pass `allowAdd={false}` to hide the inline New CTA when the record pool is fixed.

Pick from the existing pool — new records aren't allowed here.

API Reference
Props for the RecordPicker block and its field schema.

RecordPicker

PropTypeDefaultDescription
labelstringField label.
descriptionstringSub-text under the label.
requiredbooleanfalseRenders required indicator + enforces minRecords.
recordsRecordRow[]Pool of pickable records.
onRecordsChange(records: RecordRow[]) => voidFired when a record is added or edited inline.
valuestring[]Selected record ids.
onChange(ids: string[]) => voidSelection change handler.
labelKeystringRecord key used as the primary display label.
descriptionKeystringRecord key used as the secondary label.
fieldsRecordPickerFieldDef[]Schema driving the inline add/edit modal.
minRecordsnumberMinimum required selection.
maxRecordsnumberMaximum allowed selection.
addLabelstring"New..."Label of the inline new-record CTA.
allowAddbooleantrueSet false to hide the inline New CTA and block the add-record modal.

RecordPickerFieldDef

PropTypeDefaultDescription
keystringRecord property name.
labelstringForm label.
type"text" | "email" | "textarea" | "select"Renders the matching input.
options{ label: string; value: string }[]Required for `select` fields.
placeholderstringInput placeholder.
requiredbooleanfalseValidates the field on submit.