Sophic Interface Systems
Foundations

Responsive Behaviour

How product UI adapts across breakpoints — App Nav, drawers, and other content that forks by width.

How product UI adapts across phone, tablet, desktop, and ultrawide. This is the home for breakpoint behaviour — the shell fork, and any other surface that changes with width. Read it before changing App Nav, a Drawer, or inventing a new mobile/tablet split.

One primary layout fork: lg (1024px). Phone and tablet portrait share the same shell below that. Secondary widths (1366, 1920) only tweak desktop layout — they never decide bottom sheet vs right rail.

Playground

Step through the locked schematic captures (App nav / Drawer, open / closed, breakpoint):

// App nav · drawer closed// Phone · joint// /changelog/2026-08-12-shell-nav-phone.png

App nav

Product top navigation (App Nav). Source of truth in code for nav shape.

Breakpoint matrix, screenshots, and rules
ViewportWidthNo drawerDrawer open
Phone~390 (< lg)Joint icons-only, centeredHidden
Tablet portrait~768 (< lg)Same as phoneHidden
Desktop typical1024–1919 (lg+)Split track (two pills)Drawer-compact tuck (pack left on the same track)
Desktop + scroll1024–1919, scrolledJoined full-width frost, icons at both endsTuck from joined: keep frost, shrink gap only
Ultrawide≥ 1920Split trackNo tuck — split stays split

Reference screenshots

StateImage
Phone · jointnav phone
Tablet · jointnav tablet
Desktop · splitnav split
Desktop · joinednav joined
Desktop · packed (from split)nav packed
Desktop · packed (from joined)nav packed joined
Ultrawide · splitnav ultrawide

Rules (normative — change this page in the same PR if you change the code)

Below lg — joint compact

  • One centered icons-only pill (no first-name label, no hover / scroll expansion).
  • Dock Back (instrument routes) pins far left of the padded track and must not shift the pill.
  • Slightly denser icon slots below sm; a touch larger from sm through lg.
  • Directional scroll hide: down hides, up shows; within ~16px of the top always shown. Hide with a transform on the outer wrapper, never on the glass nav host (that flattens backdrop-filter in Chromium).
  • Open or exiting overlay drawer → dock fully hidden for the whole exit (useDrawerExit / OVERLAY_EXIT_MS), not icon-compact.

lg+ — split track (resting)

  • Always keep the desktop split track — never swap in the mobile joint pill for drawer-compact.
  • Rest: destinations pill + account pill with a flexible gap.
  • On scroll (unify-on-scroll): gap closes, frosts crossfade into one full-width bar; icons stay at both ends. Hover / menu still expand labels.
  • When the drawer closes, drawer-tuck must fully unwind so resting scroll-unify owns the frost again.

lg+ and < 1920 — drawer-compact tuck

Never rewrite the resting track. Only the gap and frost phases change:

  1. From split: compress first (gap closes, account slides left) → then merge frosts into one left-hugging pill.
  2. From already joined: keep the unified frost; only shrink the gap.
  3. Close: reverse. If still scroll-joined, clear tuck and reopen the gap. Otherwise unmerge, then expand.

Pack timing follows the dock expand duration (~320ms); the short unmerge beat when closing from a packed split uses the collapse duration.

≥ 1920 — no drawer tuck

The overlay rail clears the account shell; the split dock stays split with the drawer open.

Secondary desktop layout (do not invent new tiers):

WidthExtra rule
< 1366Dock Back sits in-flow beside the shells (a left gutter would clip)
≥ 1366Dock Back may sit in the left gutter when shown
< 1920Desktop drawer triggers drawer-compact tuck
≥ 1920Ultrawide — account shell clears the rail; no tuck

Drawer

Overlay Drawer from @sophic/portal-ui. Callers don’t branch on viewport — Drawer uses useLgUp() internally. Source of truth for slide axis (bottom sheet vs right rail).

Breakpoint matrix, screenshots, and rules
ViewportWidthOverlayDock while open
Phone~390 (< lg)Bottom inset sheet + washHidden
Tablet portrait~768 (< lg)Same as phoneHidden
Desktop typical1024–1919 (lg+)Right 450px rail (undimmed unless overlay)Packed left
Ultrawide≥ 1920Right 450px railSplit stays

Reference screenshots

StateImage
Phone · bottom sheetdrawer phone
Tablet · bottom sheetdrawer tablet
Desktop · right rail (+ packed dock)drawer desktop
Ultrawide · right rail (+ split dock)drawer ultrawide

Rules (normative — change this page in the same PR if you change the code)

Below lg — bottom overlay

  • Viewport-fit inset card; stops ~10% from the top.
  • Always shows a subtle dimming wash; always modal.
  • Covers most of the screen, so the App nav hides for the whole open + exit (useDrawerExit / OVERLAY_EXIT_MS).

lg+ — right rail

  • Fixed 450px rail; non-modal by default (page underneath stays interactive unless overlay is set).
  • Desktop right rails stay undimmed unless overlay is set.
  • Below 1920 the App nav packs left; at 1920+ the split dock stays put.

Frosted header and action docks (same at every breakpoint)

  • DrawerHeaderDock — title + close / back at the top; downward frost.
  • DrawerActionDock — primary actions at the bottom; upward frost.
  • Absolute docks need a relative parent. Scroll bodies need matching top / bottom padding so content doesn’t sit under the frost.

Push modes (push-reserve / push-scale / push-translate) are a separate desktop affordance — see the Drawer playground. Below sm they fall back to a fullscreen sheet via useSmUp(); that does not change the lg overlay axis above.

Trade Buy/Sell

Buy / Sell control in trade compose. Forks on lg so the bottom sheet doesn’t use the desktop folder tabs.

Breakpoint matrix and rules
ViewportControl
All viewports (column + bottom-sheet)BuySellTabs (underline Tabs — same as settings Profile nav)

Buy/Sell no longer switches between folder tabs and a segmented control by breakpoint. Market/Limit inside the ticket use the default segmented Tabs.

Holdings compact column headers

Holdings Table column headers shorten on narrow viewports. Content fork only — not a shell orientation change.

Breakpoint matrix and rules
ViewportProfit & Loss% of holdings
Below mdP&L%
md and upProfit & Loss% of holdings

Uses SlickDataTable headerCompact for the short label. Full header stays the wide label and accessibility name. See also Table.

What's inside

Weight rows (top holdings, sectors, countries, asset classes). Content fork only — the card stays full width.

Breakpoint matrix and rules
ViewportRow
Below sm (~390 phone)Name on the first line. Weight bar and percent on the next, bar filling the row.
sm and up (640+)Name, bar, and percent on one line.

Phone portrait is the tight case. Tablet portrait stays on one line — the column is wide enough.

Card headers

Section Card headers adapt to the card's width, not the viewport's. The header is a size container (@container), so the same card behaves the same on a phone, in a 370px sidebar, or in a narrow desktop column. Content fork only.

Header matrix and rules
Header widthView toggleOther actionsDetail
Below 32remChip Select (View Switch)Icon only; a second one moves to an overflow menuChevron only
32rem and upSegmented ControlUp to two visibleLabel and chevron
  • Narrow headers keep at most one visible action plus the detail chevron.
  • Switch views with View Switch; don't pair a Select and a Segmented Control by hand with lg:hidden.
  • Pass collapseBelow when a long set of options needs more room (for example a toolbar with four options collapses below xl, 36rem).
  • Outside a Section Card, make the row that holds the switch an @container.
  • Keep option labels short ("Annualized", not "Annualized returns"); the card title already names the subject.

Key files

Where to look in the repo for important pieces.

Show pieces
PieceRole
AppNavTop nav — split vs joint compact, drawer tuck (docs)
useLgUp() (portal)App-facing lg breakpoint
useLgUp() (@sophic/portal-ui)Drawer’s push vs overlay / slide axis
useSmUp() (@sophic/portal-ui)Push-drawer fullscreen below sm only
useDrawerCompactViewport()Ultrawide gate for drawer tuck (< 1920)
DrawerPicks push vs overlay
Overlay drawer modeBottom inset card (< lg) vs right 450px rail (lg+)
useDrawerExit() / OVERLAY_EXIT_MSExit coordination for shell UI
DrawerHeaderDock / DrawerActionDockFrosted top / bottom bars in detail drawers
BuySellTabsTrade Buy/Sell (underline Tabs)
SlickDataTable headerCompactShort column labels below md (e.g. P&L)
SectionCard header @containerCard header fork by card width (32rem)
ViewSwitchSegmented Control or chip Select by container width

Changelog

  • 6 October 2026 — Section Card headers fork on the card's width: View Switch collapses to a chip Select and the detail label drops below 32rem. Full notes
  • 5 October 2026 — What's inside weight rows put the bar and percent under the name below sm. Full notes
  • 25 August 2026 — Holdings % of holdings shortens to % below md (alongside P&L). Full notes
  • 12 August 2026 — Foundations page for breakpoint behaviour (shell matrix + content forks). Repo standards pointer removed; this page is the single home. Full notes

On this page