GUIDE — Mobile composition#
Status: law — v1, 2026-08-03 Applies to: the web SPA at phone widths and the native app. Mobile-first means both — most people meet Relay in a browser before they install anything. Parent: GUIDE.interface-principles.md
Design at 390px first. Not "check it on mobile after" — compose for the phone, then let the layout earn extra space on wider screens.
The space budget#
A phone gives you roughly 390–430px wide and about 700px tall, minus browser chrome. Treat it as a budget you spend, and know what each decision costs.
| Consumer | Budget | Rule |
|---|---|---|
| Horizontal navigation | 0px | The rail is off-canvas at phone width. A docked icon rail costs ~25% of the viewport and is unscannable without labels. |
| Vertical chrome above content | ≤ 2 rows | One control row + optional context line. Everything else goes in a sheet. |
| Content | everything else | If content is getting less than ~70% of the vertical space above the fold, the composition is wrong. |
Measured failure (EXP-003/004): a docked rail took ~100px of ~430px, and five stacked control layers pushed the list down until roughly one card was visible. Neither element was individually unreasonable. The composition was.
Controls: on demand, not on arrival#
A filter is useless until you know what you're filtering.
Controls do not get the position content should have. The pattern:
One control row, sticky at the top: [search] [scope] [Filter ▾]
One sheet behind the Filter button, carrying everything else — status, direction, unread, due, starred, labels, tags, sort. Filters that are active show as a count on the button (Filter · 2), so state is never hidden.
One result line — 111 relays — visible under the control row. That's orientation, not chrome.
Rules#
- No chip walls. More than ~6 options is a sheet with search, not a wrapping row of chips. 115 tags is a search problem at every width.
- One view mode. Density and layout toggles are desktop controls. Pick the right one for a phone and ship that.
- Never stack toolbars. If two control rows are both needed above the content, one of them belongs in the sheet.
Reach#
The bottom third is where hands are. Compose accordingly.
- Primary action lives in the bottom third — a bottom action bar on detail views, a FAB or sticky bar on lists that create things.
- Sticky primaries stay reachable while a long page scrolls. A detail page that requires a scroll back to the top to act has failed rule 1.
- Destructive actions are not adjacent to primary ones. Separation is the cheapest protection against the fat-finger case.
- The top bar carries orientation and escape only — title, back, overflow.
Sheets, not modals#
Bottom sheets are the default container for anything secondary: filters, actions, edit forms, pickers, confirmations that need more than a line.
| Rule | Why |
|---|---|
| Draggable, and dismissible by drag and by backdrop tap | A container the user can't dismiss by instinct reads as a trap |
| Sized to content, expandable | A half-empty full-screen sheet wastes the reveal |
| Actions inside the sheet sit at its bottom | Same reach rule applies inside the sheet |
| Never stack sheets more than one deep | A second sheet means the first one was the wrong container |
Full-screen modals are for one case only: a flow with its own multi-step lifecycle (send, claim with preview). Even then it gets a visible, labelled exit — never only a corner X.
Touch and input#
- Minimum target 44×44px, with the whole card tappable on list rows — not just the title.
- Spacing between targets ≥ 8px. Adjacent taps that do different things need daylight.
- Correct keyboard per field — numeric for numbers, no autocapitalise on Codes,
autocompleteset honestly. - Codes paste cleanly. Strip whitespace and case on paste; never make someone retype a Code because it arrived with a space.
- Inputs stay visible when the keyboard opens. Test with the keyboard up, always.
- No zoom-on-focus — inputs at ≥16px so iOS doesn't zoom.
- Respect safe-area insets. Bottom bars clear the home indicator.
Layout behaviour#
- One column. Multi-column layouts collapse to a single column with a stated priority order, not a squeeze.
- No horizontal overflow, ever. The page body never scrolls sideways. Wide content — tables, code, diagrams — scrolls inside its own container with a visible edge cue. (EXP-006 was content clipped past the card edge; that's this rule.)
- Tables become cards at phone width, or become a scrollable region with a frozen first column. They do not become tiny tables.
- Detail panes become pages. A side panel at desktop width is a navigated route on a phone, with real back behaviour.
Copy at 390px#
Copy is written for the column, not trimmed to it. See GUIDE.states-and-language.md.
- Titles: assume two lines, never rely on more.
- Buttons: one or two words. A button that wraps is a button that's wrong.
- Empty and error states: two sentences and an action. Long explanations belong behind a link.
The checklist#
Before a phone screen ships:
- Rail off-canvas — content gets full width
- ≤ 2 rows of chrome above the content
- Three or more items visible above the fold on a list
- Primary action in the bottom third and sticky if the page scrolls
- Titles unclipped at two lines
- No horizontal page scroll at 390px
- Every target ≥ 44px, whole-card tap on rows
- Tested with the keyboard open
- Tested with reduced-motion on
- Tested with a real data volume, not three seed rows
Framework note#
Everything here except the Code paste rule is generic to any phone-width object surface — framework-candidate for Launchpad Phase 2 Frontend Core.