Row detail
The most-used interaction on a business list, and the one every app reinvents: open this record, work on it, come back, open the next one. Most reinventions lose the same three things — the middle-click, the Back button, and the place in the list.
Also known as: master-detail, drill-down.
Live demo
Section titled “Live demo”Open a record from its identity cell, then walk with Previous / Next — the sequence is the result set, and the server resolves neighbours by re-running the list query, so Next crosses the page boundary between rows 3 and 4 without the client knowing pages exist.
Now tick a few rows and press Open selected: the same shape, a
different sequence. A record that disappears mid-walk renders as a
tombstone step with Next still working — this demo’s snapshot
token spells out its ids, so you can provoke it by splicing a missing
id such as 4999 into the seq= token in a record link’s URL. Mangle
the token beyond recognition instead and the snapshot fails
closed: a 410 alert with a way back to the list, never a silent
fallback to walking everything.
The link is a real link, in the identity cell
Section titled “The link is a real link, in the identity cell”<tr class="hc-datagrid__row" id="row-4903" data-row-no="137"> <td class="hc-datagrid__cell"> <input class="hc-checkbox" type="checkbox" name="ids" value="4903" aria-label="Select order SO-4903"> </td> <th class="hc-datagrid__cell" scope="row"> <a href="/orders/4903" data-hc-row-link>SO-4903</a> </th> <!-- …the data cells… --></tr>The identity column carries it — the thing the row is, not a verb appended to it. Being an ordinary anchor buys middle-click, ⌘-click, copy-address, Back, the keyboard and the no-JS path; a click handler re-implements all of that badly.
installRowLink() adds the only thing an anchor cannot do by itself:
Enter anywhere on the row, which is what a grid user expects after
arrowing to it. Editing wins where it applies (the datagrid cancels the
event before opening an editor), a control that owns its Enter keeps
it, and a modifier means the user asked for something else.
Coming back is the part everyone drops
Section titled “Coming back is the part everyone drops”The list URL already carries the conditions, the sort, the columns and
the page. What Back cannot restore is which row — so the detail’s
Back to list is that URL plus #row-<id>:
/orders?f-ship=@week-start..&sort=-ship,order&page=4#row-4903installDatagrid()
lands the active cell there and scrolls the grid’s own scrollport,
so the keyboard user arrives where the eye does.
| State | Restored by |
|---|---|
| conditions, sort, columns, page | the list URL |
| which row | #row-<id> |
| selection ticks | only when the trip started from a selection |
| scroll offset | nothing, deliberately — focus beats a pixel offset, which points at a different row after any insert |
| an in-progress edit | nothing — unsaved changes warns before leaving |
The detail needs to know that URL: either the app knows the canonical
list, or the link carries ?from=<encoded url> — validated
server-side (same origin, known route) before being echoed into a
link, because an unvalidated from is an open redirect.
Returning fresh beats returning identical
Section titled “Returning fresh beats returning identical”| Case | Return |
|---|---|
| nothing changed | Back — the browser’s own history is the cheapest correct restore |
| something was saved | 303 to the list URL + #row-<id> |
The redirect is not ceremony. A restored snapshot shows the data as it was before the user’s own edit — the one stale value they are guaranteed to notice — and the pager totals go with it.
Peek or page: one URL, two renderings
Section titled “Peek or page: one URL, two renderings”<a href="/orders/4903" data-hc-row-link data-hx-get="/orders/4903?peek=1" data-hx-target="#record-dialog" data-hx-swap="innerHTML">SO-4903</a>The href stays canonical, so JavaScript failing means a full page and
not a dead row. The overlay is the
remote-dialog recipe,
and it contains a link to the full page — a peek that traps you is
worse than no peek.
Page, peek, or docked pane
Section titled “Page, peek, or docked pane”A modal is not the answer to “where does the detail go” — it is one of three, and the page is the default, because it is the only one with a URL:
| Shape | When | Why |
|---|---|---|
| a page (default) | the record has fields, sub-grids, its own actions; the user is in it | it is the only rendering that can be shared, bookmarked, opened in a second tab, or reached without JavaScript |
| a docked pane (split) | the work is comparing record and list, over and over (triage, matching) | both stay live; nothing is hidden behind an overlay |
| a peek (modal) | glance and go — check one field, approve, close — on a short record | keeps the list’s place. The moment it needs to scroll, or grows tabs or a sub-grid, it wanted to be a page |
A modal is the least common of the three in business software, and that is not an accident. Opening a record replaces the screen in Gmail; SAP Fiori splits list and record into columns; Salesforce and ServiceNow give the record a page. Modals in those products are for short, self-contained tasks — create one thing, confirm, edit a field — not for “the record”, because a record is where the work happens and work needs room, a URL and its own error surfaces.
The failure mode to avoid is specific: a modal with no URL. If the address bar still says list while the user is looking at a record, then Back closes something they did not open, the link they send a colleague is the wrong screen, and refreshing loses their place. A modal that pushes a URL and restores itself from one is a legitimate rendering; a modal that does not is a dead end wearing a detail screen’s clothes.
Two rules keep the peek honest, and both are failures if broken:
- The row’s
hrefis the page, and the peek is layered on top of it withdata-hx-get. A record reachable only through a dialog cannot be linked, which is the thing colleagues ask for most. - The peek contains a link to the page. A peek that traps you is worse than no peek.
Walking a sequence
Section titled “Walking a sequence”Opening one record is half the job; business work is a queue. The detail carries prev / next, and the sequence is whatever the user was looking at:
| Sequence | Wire |
|---|---|
| the result set (default) | ?seq=list&i=<ordinal> — the server resolves neighbours by re-running the list query, so “next” crosses a page boundary without the client knowing pages exist |
| the selection | “Open selected (12)” submits the ids checkboxes → POST /orders/selections → 303 with a token |
The token names an ordered snapshot and the screen says so
(Record 3 of 12 selected at 14:32). A missing record is a tombstone
step, not a wall — aborting at the first gap makes the feature
untrustworthy exactly when data is moving — and an expired token
fails closed (410 + a link back), never a silent fallback to
walking everything.
Where the walk goes — and why not the bottom
Section titled “Where the walk goes — and why not the bottom”Prev / next live in the record’s header, beside its identity and Back to list. They are not pagination of the content below them:
- The decision to move on is usually made before reading to the bottom — glance, judge, next. A control you must scroll to reach turns a two-second judgement into a scroll, every time.
- The detail body scrolls, so a bottom control either scrolls away — useless exactly when the queue is long — or needs a second fixed strip, which is chrome bought for a rarely-reached position.
- After a save the
303lands the user at the top, so the next move should be where they already are.
A long detail may repeat them at the bottom as a secondary copy; both are the same links, so there is no state to keep in sync, and the header stays primary.
Within that header, the arrangement carries meaning too, and it is the one every mail client already taught people:
← Back to list …record identity… 1 / 15,129 ‹ ›- the exit goes at the start, because that is where a person looks to get out;
- the walk — the position, then prev / next — goes at the end, where the pointer already is and where moving on is the frequent act.
Same rule as the list’s navigation strip: where you are and where you go at the end, the way out at the start.
A grid inside a detail pages itself
Section titled “A grid inside a detail pages itself”Detail screens hold grids of their own — lines, shipments, history — and each pages directly under itself, never in a page-level footer. A page-level pager on a screen with three grids cannot say which grid it pages; the list template’s rule — navigation under the data it moves through — is about that data, not about the page.
Only the grid carrying
hc-fill
takes the remaining height; the others keep their own caps, their own
pagers and their own empty states.
So the bottom of a detail screen carries its actions — Save, Cancel — not navigation.
Related
Section titled “Related”- Datagrid — row ordinals, fragment navigation, the active cell
- Datagrid bulk actions — where the selection comes from
- Remote dialog — the peek rendering
- Data grid page — the screen this happens on