Detail pages
A list gets someone to a record; the detail page is the record. This is the transaction page, measured: a 1024px column centred in the island, the receipt on the left at 576px, a 264px properties rail on the right that sticks while the receipt scrolls. Customers and payouts are the same page with different panels.
The receipt opens with a hero — the order number and status pill on one line, then the amount at 44px with the date beside it — and continues as panels, each a tiny uppercase header over a bordered card. The rail opens with the record’s actions as a cluster of chips, then Properties: the facts you look up rather than read, as icon-led rows with no labels. Everything else waits in disclosures.
.folio-detail$77.50
Sep 1, 2026 at 10:07 PM
Order
After Effects Bundles
Qty 1 · $250.00
Notes
The rail's chips are grey at rest and only turn red on hover, so a record you are merely looking at does not read as a wall of danger.
import {
DetailPage, DetailMain, DetailAside, DetailHero, DetailSkeleton,
Block, BlockRow, BlockFoot, BlockLine, BlockText,
RailActions, RailAction, Properties, Property, Disclosures, Disclosure, Field,
} from '@/components/detail-page'
if (loading) return <DetailSkeleton actions={2} /> // the same containers, so nothing moves
<DetailPage>
<DetailMain>
<DetailHero label="#00244" badge={<Badge variant="success">Succeeded</Badge>} title="$77.50" meta="Sep 1, 2026 at 10:07 PM" />
<Block title="Order">
<BlockRow tile={<img src={image} />} title="After Effects Bundles" dim="Qty 1 · $250.00" value="$250.00" />
<BlockFoot>
<BlockLine label="Subtotal" value="$250.00" />
<BlockLine label="Discount (AEEFFECTS)" value="-$172.50" tone="success" />
<BlockLine total label="Total" value="$77.50" />
</BlockFoot>
</Block>
</DetailMain>
<DetailAside>
<RailActions>
<RailAction icon={<UndoSolid />} destructive onClick={refund}>Refund</RailAction>
</RailActions>
<Properties>
<Property lead icon={<UserSolid />}>Card Holder Name</Property>
<Property icon={<MailSolid />} href="mailto:[email protected]" copy="[email protected]">[email protected]</Property>
<Property icon={<CalendarSolid />}>Sep 1, 2026 at 10:07 PM</Property>
</Properties>
<Disclosures>
<Disclosure title="Identifiers">
<Field label="Transaction ID" value={id} copy={id} />
</Disclosure>
</Disclosures>
</DetailAside>
</DetailPage>Actions that are not rail chips — and the chips again, out of habit — go in the dashboard’s header bar. The transaction page does exactly this: the rail is where you find Refund, the ⋯ menu is where you reach for it.
useTitleBar({
crumbs: [
{ id: 'list', label: 'Deliveries', onClick: () => nav('/deliveries') },
{ id: 'one', label: title, loading: !loaded }, // skeleton pill until the record arrives
],
menu: [
{ label: 'Send again', icon: 'send', onAction: resend },
{ label: 'Revoke', icon: 'trash', destructive: true, onAction: revoke },
],
})Rules
- The hero states the record: label (an order number) at 20px muted, the headline (an amount or a name) at 44px, the status beside the label, the date beside the headline. Never a page title repeating the breadcrumb.
- Panels are an uppercase 12px header over a bordered, shadowed card. Rows inside are 44px tiles with a title and a dim sub line; the totals band is tinted and separated by a hairline.
- Rail chips are 32px, filled, muted. Destructive ones only turn red on hover. Two or three at most; more than that belongs in the ⋯ menu.
- Properties have no labels. A glyph says what a row is; the value is the row. Anything that needs a label or a second line is a
Fieldinside aDisclosure. - Load behind
DetailSkeleton: it is built from the same containers, so the columns do not move when the record lands.