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.
Alex Wong
Installation
$Terminal
npx shadcn@latest add https://sdk-components.thesqd.com/r/record-picker-block.jsonUsage
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 recordsMembers — 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.
Alex Wong
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
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Field label. |
description | string | — | Sub-text under the label. |
required | boolean | false | Renders required indicator + enforces minRecords. |
records | RecordRow[] | — | Pool of pickable records. |
onRecordsChange | (records: RecordRow[]) => void | — | Fired when a record is added or edited inline. |
value | string[] | — | Selected record ids. |
onChange | (ids: string[]) => void | — | Selection change handler. |
labelKey | string | — | Record key used as the primary display label. |
descriptionKey | string | — | Record key used as the secondary label. |
fields | RecordPickerFieldDef[] | — | Schema driving the inline add/edit modal. |
minRecords | number | — | Minimum required selection. |
maxRecords | number | — | Maximum allowed selection. |
addLabel | string | "New..." | Label of the inline new-record CTA. |
allowAdd | boolean | true | Set false to hide the inline New CTA and block the add-record modal. |
RecordPickerFieldDef
| Prop | Type | Default | Description |
|---|---|---|---|
key | string | — | Record property name. |
label | string | — | Form label. |
type | "text" | "email" | "textarea" | "select" | — | Renders the matching input. |
options | { label: string; value: string }[] | — | Required for `select` fields. |
placeholder | string | — | Input placeholder. |
required | boolean | false | Validates the field on submit. |