Record Layout
A record page's main column beside a sticky right rail of cards, with a Details sheet in its place on small screens.
- Status
- Since
0.23.15- Accessibility pattern
- complementary landmark, dialog on small screens
Last updated
Weekly sync with Acme
Install
Add Record Layout from the VegaStack registry. The CLI verifies the item's integrity hash before writing it.
pnpm dlx shadcn@latest add @vegastack/record-layoutThe same command installs the registry items it composes: @vegastack/button, @vegastack/sheet, @vegastack/skeleton.
Usage
import {
RecordDetailsSheet,
RecordLayout,
RecordLayoutMain,
RecordLayoutRail,
} from "@/components/ui/record-layout";
<RecordLayout>
<RecordLayoutMain>
<PageHeader
title="Weekly sync"
actions={<RecordDetailsSheet>{facts}</RecordDetailsSheet>}
/>
<Tabs>…</Tabs>
</RecordLayoutMain>
<RecordLayoutRail aria-label="Details">
<Card size="sm">…</Card>
</RecordLayoutRail>
</RecordLayout>;RecordLayout is a record page's frame: the main column (title, status line, tabs) and, from lg
up, a 320px right rail of cards — a PropertyList of the
record's facts first, related work below. The rail sticks to the top of the scroll container and
scrolls on its own when it is taller than the viewport; set --record-rail-offset to the height
above it. Below lg the rail is hidden, and RecordDetailsSheet — a ghost ⓘ icon button beside the
title — opens the same facts in a Sheet from the right, full width on a phone. Anything else from
the rail moves into the main column, usually as a tab.
The switch is CSS, so the layout is server-safe. When a rail card holds state that must exist only
once (a selection, pending writes), mount it in one place with useMediaQuery.
Weekly sync with Acme
Anatomy
Examples
Actions in a tab on small screens
A rail card that holds state — suggestions being selected, writes waiting on Undo — mounts once:
in the rail from lg up, else as the last tab of the main column.
const wide = useMediaQuery("(min-width: 1024px)");
const actions = <MeetingActions … />;
<RecordLayout>
<RecordLayoutMain>
<Tabs>
<TabsList>
<TabsTrigger value="summary">Summary</TabsTrigger>
{!wide && <TabsTrigger value="actions">Actions</TabsTrigger>}
</TabsList>
{!wide && <TabsContent value="actions">{actions}</TabsContent>}
</Tabs>
</RecordLayoutMain>
{wide && <RecordLayoutRail aria-label="Details and actions">{actions}</RecordLayoutRail>}
</RecordLayout>;Tabs, counts and loading
RecordTabsRow puts the TabsList and the tab's icon actions (RecordTabsActions: Copy, Edit) on one row at the main column's width. The rail follows the layout's own width (a container query at 1024px), not the viewport; useRecordLayoutWide() gives the same answer to script, for mounting rail content once. RecordLayoutPanels holds the main column's TabsContent panels under the tab row (spread
useTabsSwipe() onto it), RecordTabCount puts a muted count in a TabsTrigger, and
RecordLayoutMainSkeleton stands in for the main column while the record loads.
<Tabs value={tab} onValueChange={setTab}>
<RecordTabsRow>
<TabsList>
<TabsTrigger value="summary">Summary</TabsTrigger>
<TabsTrigger value="actions">
Actions <RecordTabCount count={3} label="to review" />
</TabsTrigger>
</TabsList>
<RecordTabsActions>{/* Copy, Edit */}</RecordTabsActions>
</RecordTabsRow>
<RecordLayoutPanels {...swipe}>
<TabsContent value="summary">…</TabsContent>
</RecordLayoutPanels>
</Tabs>API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | What the sheet shows — usually the same PropertyList as the rail's first card. |
className | string | — | Classes for the ⓘ trigger button. |
onOpenChange | ((open: boolean) => void) | — | Called when the sheet opens or closes. |
open | boolean | — | Controlled open state. |
title | string | "Details" | The sheet's title, and the trigger's accessible name. |
RecordLayout, RecordLayoutMain and RecordLayoutRail take their element's native props.
Accessibility
- The rail is an
aside(a complementary landmark): name it witharia-label. - The ⓘ trigger is named by
title("Details"); the sheet is a modal dialog titled the same, and focus returns to the trigger when it closes.
| Key | Action |
|---|---|
| Enter / Space | Open the Details sheet |
| Esc | Close the sheet |
| Contract | States tested |
|---|---|
| Behaviour | rail, details-sheet |
| Accessibility | labeled, browser-accessibility-test |
| Visual | default |
Do / Don't
App Shell
A shared dashboard layout with skip-linked sidebar, header, and scrollable main region composed from VegaStack navigation primitives.
Comments
A record's comments — a list with a count, an Oldest or Newest first toggle, skeleton and empty states, in-place editing, and a Linear-style composer.