Record Peek
A record opened beside its list — non-modal and resizable, ↑/↓ and J/K to walk, ⌘↵ for the full page, full-screen on a phone.
- Status
- Since
0.25.88- Accessibility pattern
- named panel, keyboard walk, focus returns to the row
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 Peek before installation. Retain the digest printed by this command.
pnpm exec vegastack-design verify record-peekInstall with the pinned shadcn CLI. Bare shadcn does not verify VegaStack signature or integrity.
pnpm dlx shadcn@4.21.0 add @vegastack/record-peekRun 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/direction, @vegastack/drawer, @vegastack/kbd, @vegastack/tooltip, @vegastack/use-platform.
Usage
import { RecordPeek, RecordPeekBody } from "@/components/ui/record-peek";
import { RecordHeader } from "@/components/ui/record-header";
<RecordPeek
open={peekId != null}
onOpenChange={(open) => !open && closePeek()}
label="Task"
position={{ index, total: order.length }}
onPrevious={index > 0 ? () => openPeek(order[index - 1]) : undefined}
onNext={
index < order.length - 1 ? () => openPeek(order[index + 1]) : undefined
}
fullLink={<Link href={`/tasks/${peekId}`} />}
actions={<TaskActionsMenu id={peekId} />}
storageKey="peek-width"
// The row it was opened from — kept when the peek opens, since `peekId` is gone by close time.
returnFocus={() => originRow.current}
>
<RecordPeekBody>
<RecordHeader level={2} keyBadge={task.ref} title={task.title} />
<TaskSections task={task} />
</RecordPeekBody>
</RecordPeek>;Scope
The one way a record opens from a list: tasks, table rows, meetings, customers — any record the
app registers. The peek knows no record kinds; the app passes the noun (label), where the record
sits in its list (position), how to walk it (onPrevious, onNext), its full page (fullLink,
fullHref or onOpenFull), its actions and its body. Documents and files keep opening as a full
page or the file viewer.
Anatomy
Examples
A list and its peek
Click a row to open it. While the peek is open, ↑/↓ and J/K walk the list (never while typing, while
a menu, popover or another dialog is open, and ↑/↓ never from a control that uses them — a grid, a
tab list, a select), "n of N" follows, the buttons are disabled at the ends, ⌘↵ /
Ctrl+↵ opens the full page and Esc closes the peek and puts focus back on the row it came from.
The list stays usable: a click on another row switches the record. The open row carries
data-peek-active — DataList, DataGrid and Board set it from activeRowId; any other list
uses useRecordPeekRow, which also scrolls the row into view as the peek walks.
Width and phones
The separator on the start edge resizes the panel from 480px up to 90% of the window (minWidth,
maxShare), by pointer or with ← → (32px), Home and End; storageKey remembers the width in this
browser. Below the tablet tier (40rem) the peek is the whole screen, edge to edge, padded by the
safe areas, and the separator goes.
<RecordPeek storageKey="regent:peek-width" minWidth={480} maxShare={0.9} … />API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
label* | string | — | The record's kind as a noun ("Task", "Row") — the panel's accessible name, and the noun in the previous/next labels. |
onOpenChange* | (open: boolean) => void | — | Called when the peek asks to close (Esc, the close button) — and with true never. |
open* | boolean | — | Whether the peek is open. |
actions | React.ReactNode | — | The header's end slot before open-full and close: the record's ⋯ menu, a Share button. |
children | React.ReactNode | — | The record body: RecordPeekBody holding a RecordHeader and the record's sections. |
closeLabel | string | "Close" | |
defaultWidth | number | 640 | The starting width when uncontrolled, in px. |
formatPosition | ((index: number, total: number) => string) | (index, total) => `${index + 1} of ${total}` | "n of N" text for the position, for another language. |
fullHref | string | — | The record's full page. Rendered as a link (pass fullLink for a router link); ⌘↵ / Ctrl+↵
follows it. |
fullLabel | string | "Open full page" | |
fullLink | React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | — | A router link to render the open-full button as (<Link href="/tasks/42" />); its href
wins over fullHref for the button, and ⌘↵ clicks it. |
keyboard | boolean | true | Bind ↑/↓, J/K and ⌘↵ / Ctrl+↵ on the document while open — never while typing, while a menu, list box, popover or another dialog is open, and ↑/↓ never from a control that uses them (a grid, a tab list, a select trigger). |
maxShare | number | 0.9 | The widest the separator goes, as a share of the window. |
minWidth | number | 480 | The narrowest the separator goes, in px. |
onNext | (() => void) | — | Walk to the next record (button, ↓, J). Omit when there is none. |
onOpenFull | (() => void) | — | Opens the full page instead of a link (a client navigation the app owns). |
onPrevious | (() => void) | — | Walk to the previous record (button, ↑, K). Omit when there is none. |
onWidthChange | ((width: number) => void) | — | Called as the separator moves the width (pointer, or ← → / Home / End on it). |
position | RecordPeekPosition | — | The open record's place in its list; shows "3 of 41" and disables previous/next at the ends. |
returnFocus | HTMLElement | (() => HTMLElement | null) | null | — | Where focus goes when the peek closes: the row it was opened from. A function is read at close time — keep the row (or its id) from when the peek opened, since the open record's id is usually cleared by then. |
storageKey | string | — | Remembers an uncontrolled width in localStorage under this key, per browser. |
width | number | — | The panel width in px (controlled). Pair with onWidthChange. |
Data attributes and CSS variables on RecordPeek
| Attribute | Values |
|---|---|
data-slot | "record-peek" | "record-peek-actions" | "record-peek-close" | "record-peek-header" | "record-peek-next" | "record-peek-position" | "record-peek-previous" | "record-peek-resize" |
--record-peek-max | CSS custom property |
--record-peek-width | CSS custom property |
RecordPeekBody
RecordPeekBody takes a div's props: the scrolling body below the header, padded like a record
page's main column.
useRecordPeekRow
| Prop | Type | Default | Description |
|---|---|---|---|
data-peek-active* | "" | undefined | — | "" on the open row, absent otherwise — the attribute every list styles the same way. |
ref* | (instance: T | null) => void | (() => VoidOrUndefinedOnly) | React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES[keyof React.DO_NOT_USE_OR_YOU_WILL_BE_FIRED_CALLBACK_REF_RETURN_VALUES] | — | Attach to the row, so it can scroll into view when it becomes the open one. |
Accessibility
| Key | Does |
|---|---|
| ↓ / J | Next record (while open; not in a field, menu or popover) |
| ↑ / K | Previous record |
| ⌘↵ / Ctrl+↵ | Open the full page |
| Esc | Close; focus returns to the row (returnFocus) |
| ← / → on the separator | Wider / narrower by 32px; Home and End go to the bounds |
- The panel is a non-modal dialog named by
label; the page behind it stays in the tab order. - The buttons carry
aria-keyshortcuts, and the open-full tooltip shows the platform's chord. - The separator is a focusable
role="separator"with its value, minimum and maximum.
| Contract | States tested |
|---|---|
| Behaviour | closed, open, first, last, resizing, phone |
| Accessibility | labeled |
| Visual | default, with-icon |