Record Chip
A picker chip that shows which record something belongs to — icon, name and ▾ — with a ↗ link to that record.
- Status
- Since
0.23.10- Accessibility pattern
- picker button plus a named link
Last updated
Install
Add Record Chip from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/record-chipThe same command installs the registry items it composes: @vegastack/button, @vegastack/popover.
Usage
import { RecordChip } from "@/components/ui/record-chip";
<PopoverTrigger
render={
<RecordChip
icon={<Building2 />}
value={customer?.name}
placeholder="Add customer"
href={customer ? `/customers/${customer.id}` : undefined}
linkLabel={`Open ${customer?.name}`}
renderLink={(props) => <Link {...props} />}
/>
}
/>;Every prop not listed below — and the ref — goes to the picker button, so the chip drops into any
trigger's render (Popover, DropdownMenu, Combobox).
Anatomy
Examples
Customer and project
A customer chip, and a project chip that appears once a customer is set. An empty chip has a dashed border and the placeholder; a set chip adds the ↗ link to the record.
Split chip anatomy
RecordChip is built on SplitChip, exported from the same file, for any pill that holds a main
action plus a secondary icon action. The pill carries one shared p-0.5 inset, and every segment is
24px tall and rounded-full, so each hover or open background sits the same 2px from the border on
every side. Segments are divided by SplitChipSeparator.
import {
SplitChip,
SplitChipButton,
SplitChipSeparator,
splitChipIconActionClassName,
} from "@/components/ui/record-chip";
<SplitChip>
<SplitChipButton onClick={pick}>
Acme <ChevronDown />
</SplitChipButton>
<SplitChipSeparator />
<Link
href="/customers/acme"
aria-label="Open Acme"
className={splitChipIconActionClassName}
>
<ArrowUpRight />
</Link>
</SplitChip>;Do not add padding, margins or heights to the segments — the inset belongs to the pill.
Picker menu
RecordChipMenu is the popover a pill opens: start-aligned with no inner padding, 288px for a
searchable Command list (width="list") or as wide as a Calendar (width="fit").
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
href | string | — | Opens the linked record. With a value, a ↗ link sits after the picker. |
icon | React.ReactNode | — | The record type's icon (a building for a customer, a folder for a project). |
linkLabel | string | "Open" | Accessible name of the ↗ link. |
placeholder | React.ReactNode | "Select" | Shown when there is no value, e.g. "Add customer". null shows the icon alone (no text, no
▾) — an unset pill in a dense row; name the button with aria-label. |
renderLink | ((props: { href: string; className: string; "aria-label": string; children: React.ReactNode; }) => React.ReactNode) | a plain `<a>` | Renders the ↗ link — pass your router's link for client navigation. |
value | React.ReactNode | — | The linked record's name. Empty shows placeholder in the muted ink. |
Data attributes and CSS variables on RecordChip
| Attribute | Values |
|---|---|
data-empty | "" |
data-slot | "record-chip" | "record-chip-trigger" |
Accessibility
- The picker is a
button; the trigger it is rendered into addsaria-expandedandaria-haspopup. Give it anaria-labelsuch as "Customer: Acme" when the icon is the only thing that says which kind of record it is. - The ↗ link is a separate tab stop named by
linkLabel("Open Acme").
| Contract | States tested |
|---|---|
| Behaviour | default, empty, linked, disabled |
| Accessibility | labeled |
| Visual | default, empty, linked, disabled |