---
id: container-choice
class: b2
sourced: 2026-07-12
source: "b2b-ux-patterns ch.8"
license: open
---
# Pass: detail surface & container choice

**Inputs:** per record-opening — `opensRecord`, `contextRetentionNeeded`, `interactionDepth` (quick-read | deep-edit), `sharing` (stable-url | sent-outward | none), `traversal`, `interruption` (interrupt | continue), `contentSize` (small | large), `recordReference`, `opensInOverlay`; per detail surface — `isDetailSurface`, `detailBody` (large-independent | modest-cross-skim), `longSessionStream`, `customerFacingComposer`.
**Owns:** `containers`, `requiredMechanics`, `clarifications`.
**Output artifact:** the record-detail anatomy (sticky header, body organization, related lists, timeline, composer, context rail) and, for every place a record opens, the container rung it opens at — chosen by workflow, never by information size.

## The rule

**A detail page is a workstation, not a read-only profile.** Every detail surface carries a *sticky record header* — identity, state chip, and the 1–3 highest-frequency legal verbs for the record's current state and this viewer (from the verbs pass), the rest in overflow — pinned so identity never scrolls away. Fields *edit in place* by default (optimistic save with saving/saved/failed feedback); an explicit whole-record edit mode is reserved for interdependent fields that must commit atomically. Related lists are *action surfaces* — inline create, inline edit, row verbs, and a "view all" escape — and they are how demoted objects honor the reachability half of the demotion contract. The activity timeline is *derived*, never designed: Logged fields contribute attributed change entries, Sampled fields contribute trends, comments and notes are ordinary child objects; you declare history classes and the feed renders. A customer-facing composer carries **audience safety** — an internal note and a customer-facing send are different verbs with distinct affordance and color, never a toggle buried in one composer; sending to the wrong audience is a severity-1 failure.

**Body organization forks two ways.** *Tabs* (lazy-loaded, per-section linkable) when sections are large, independently loaded, and rarely needed together — the record is "several pages sharing a header." *Single-scroll with a sticky anchor rail* when the job needs cross-section skimming in one pass and sections are modest. Objects worked in long sessions over a streaming main column (conversation, timeline) earn a persistent **360 / context rail** — the parent summary and key attributes stay put while the narrative moves; the rail is denormalization-for-display doing its job.

**The container ladder — `hovercard → inline-popover → center-modal → side-peek → full-page`.** Information size does not pick the rung; five workflow axes do, size only breaks ties. (1) **Context retention** — if the originating list must stay live behind the opened record (open, close, open the next, compare), it is a **side peek**, however small the record; a modal dims and blocks the hub, a peek keeps it alive. (2) **Read-vs-edit depth** — a quick read wants a peek; deep, multi-field, interdependent editing wants center focus or a full page, because accidental dismissal becomes expensive. (3) **Shareability** — a record that needs a stable, shareable URL can now live in a URL-addressable peek or split pane; a record **sent outward** (an invoice) still defaults to a **full page** despite tiny size. (4) **Traversal** — processing or comparing many records in a row needs next/prev through the *exact filtered, sorted set*, which means a peek or split pane, never a modal and never a full-page round-trip per record. (5) **Interruption** — a center modal *blocks* (correct for confirmations and irreversible commits); a peek *continues* (work proceeds around it). Size is the tiebreaker only when the axes are indifferent: a leaf datum in a popover, a modest record in a peek.

**Container requirements.** Every peek and modal carries an **"open as full page"** escape hatch — users promote up the ladder themselves, so picking the lighter rung costs one click. Closing a peek or modal honors the **return contract**: the originating surface restores *exactly as left* — same scroll, filters, selection. The same object opens at the same rung from the same kind of origin, product-wide — container choice is per-object-per-origin, not per-screen improvisation. When a record must open but no workflow axis is declared, the rung cannot be chosen from size alone: clarify.

```json decision-table
{"pass": "container-choice", "rows": [
  {"when": {"contextRetentionNeeded": true}, "then": {"containers": {"acceptable": ["side-peek", "split-pane"], "forbidden": ["center-modal"]}}, "reason": "hub-stays-live-is-a-peek"},
  {"when": {"interactionDepth": "deep-edit"}, "then": {"containers": {"acceptable": ["full-page", "center-modal"], "forbidden": ["side-peek"]}}, "reason": "deep-edit-wants-committed-focus"},
  {"when": {"interactionDepth": "quick-read", "contextRetentionNeeded": false, "traversal": false}, "then": {"containers": {"acceptable": ["side-peek", "inline-popover"]}}, "reason": "quick-read-is-a-peek"},
  {"when": {"sharing": "sent-outward"}, "then": {"containers": {"acceptable": ["full-page"], "forbidden": ["center-modal", "side-peek"]}}, "reason": "sent-records-get-a-page"},
  {"when": {"sharing": "stable-url"}, "then": {"requiredMechanics": {"mustInclude": ["stable-shareable-url"]}}, "reason": "url-addressable-rungs-are-fine"},
  {"when": {"traversal": true}, "then": {"containers": {"acceptable": ["side-peek", "split-pane"], "forbidden": ["center-modal", "full-page"]}, "requiredMechanics": {"mustInclude": ["next-prev-through-filtered-set"]}}, "reason": "traverse-the-filtered-set-needs-a-peek"},
  {"when": {"interruption": "interrupt"}, "then": {"containers": {"acceptable": ["center-modal"]}}, "reason": "blocking-is-the-point"},
  {"when": {"interruption": "continue"}, "then": {"containers": {"forbidden": ["center-modal"]}}, "reason": "work-continues-around-a-peek"},
  {"when": {"recordReference": true}, "then": {"containers": {"acceptable": ["hovercard"]}, "requiredMechanics": {"mustInclude": ["hovercard-read-only-no-actions"]}}, "reason": "fk-resolution-is-a-hovercard"},
  {"when": {"contentSize": "small", "interactionDepth": "quick-read"}, "then": {"containers": {"acceptable": ["inline-popover", "hovercard", "side-peek"]}}, "reason": "smaller-content-sits-lower"},
  {"when": {"opensInOverlay": true}, "then": {"requiredMechanics": {"mustInclude": ["escape-hatch-open-as-full-page", "return-contract-restore-state", "consistent-rungs-per-object"]}}, "reason": "peek-and-modal-carry-escape-and-return"},
  {"when": {"isDetailSurface": true}, "then": {"requiredMechanics": {"mustInclude": ["sticky-record-header", "inline-edit-in-place-default", "related-lists-action-surface", "activity-timeline-derived"]}}, "reason": "detail-is-a-workstation"},
  {"when": {"detailBody": "large-independent"}, "then": {"requiredMechanics": {"mustInclude": ["tabs-lazy-panels"], "mustNotInclude": ["single-scroll-anchor-rail"]}}, "reason": "independent-sections-tab"},
  {"when": {"detailBody": "modest-cross-skim"}, "then": {"requiredMechanics": {"mustInclude": ["single-scroll-anchor-rail"], "mustNotInclude": ["tabs-lazy-panels"]}}, "reason": "cross-skim-single-scroll"},
  {"when": {"longSessionStream": true}, "then": {"requiredMechanics": {"mustInclude": ["context-rail-360", "denormalized-parent-summary"]}}, "reason": "durable-context-beside-a-moving-stream"},
  {"when": {"customerFacingComposer": true}, "then": {"requiredMechanics": {"mustInclude": ["activity-composer-audience-safety"], "mustNotInclude": ["audience-toggle-single-composer"]}}, "reason": "internal-note-and-send-are-different-verbs"},
  {"when": {"opensRecord": true, "interactionDepth": "__absent__", "contextRetentionNeeded": "__absent__", "sharing": "__absent__", "traversal": "__absent__", "interruption": "__absent__"}, "then": {"clarifications": ["container-workflow-unspecified"]}, "reason": "container-picked-by-workflow-not-size"}
]}
```
