Breakpoints, the column a screen sits in, when two panes earn their place, and why the tab bar stays at the bottom on an iPad.
Part of the Konjo design language. The laws in the spine apply here unless this chapter contradicts them.
Kata decided that every screen has a designed tablet layout (the interview). The dojo's iPad at the front desk, the instructor's iPad on the mat, a parent's tablet on the couch — they all ran the phone layout stretched edge to edge: a 64pt row 1,180 points wide, a chevron a forearm away from its label, a hero slab the width of a whiteboard. This chapter is how that stops.
A phone does not change. Everything here switches on at breakpoint.tablet and is a no-op
below it. If a phone screenshot moves by a pixel, the change is wrong.
Three size classes, read from the window
| Class | Window width | Gutter | What changes |
|---|---|---|---|
| Phone | below breakpoint.tablet (700) |
spacing.page 20 |
Nothing. This is the layout every screen was designed on. |
| Tablet | breakpoint.tablet 700 and up |
layout.gutterTablet 32 |
Content sits in a centred column; forms in a narrower one; grids add columns. |
| Wide | breakpoint.wide 1024 and up |
layout.gutterWide 40 |
As tablet, plus list/detail screens may split into two panes. |
Widths, not devices. An iPad in Split View or Slide Over reports a phone width and gets the
phone layout, which is right: the space is what matters, not the hardware. Never branch on
Platform.isPad, a model name, or a screen's physical size.
700, not 768. Class Mode shipped its tablet layout on a 700pt seam (useIsWide), and two
thresholds a few dozen points apart would put an iPad mini in portrait (744) on the tablet layout
for one screen and the phone layout for the next. There is one number. useIsWide now reads it
from the token.
Read it from useLayout(), never from
useWindowDimensions and a comparison you wrote yourself. It re-renders on rotation, on a Split
View resize and on a browser window being dragged, and it hands back isTablet, isWide,
gutter, contentMaxWidth and the two column styles below.
The column
On a tablet a screen's content sits in one centred column, layout.readableWidth (720) wide
plus the screen's own spacing.page padding on each side. Past ~720pt a 16pt line runs well
over ninety characters and a row's chevron drifts so far from its label that the two stop
reading as one control. The column narrows further when the window does, so the visible gutter
is never less than the size class's gutter: at 820 (iPad portrait) the text edge sits 50pt in; at
1180 (landscape) the column is centred with the page tint either side.
phone 390 tablet 820 wide 1180
┌──────────┐ ┌──────────────────────────┐ ┌────────────────────────────────────┐
│ Title │ │ Title ◎ ⬤ │ │ Title ◎ ⬤ │
│ ▓▓▓▓▓▓▓▓ │ │ ╭▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓╮ │ │ ╭▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓╮ │
│ Row › │ │ Row › │ │ Row › │
│ Row › │ │ Row › │ │ Row › │
└──────────┘ └──────────────────────────┘ └────────────────────────────────────┘
edge to edge column 760, gutters ≥32 column 760, centred on the tint
How a screen adopts it:
- A scroll spreads
columnStyleinto itscontentContainerStyle, after the screen's own style. It isundefinedon a phone, so the phone's style is untouched. Do not wrap the scroll's children in a View instead — agapon the container would then apply to one child. - A header above the scroll takes the same column so the title lines up with the content.
ScreenHeaderdoes this itself; aChildHeadertakesstyle={columnStyle}; a hand-drawn header row spreadscolumnStyleinto its style. - Anything else outside the scroll — a search row, filter chips, a footer — goes in
<ScreenContainer>, which is a bare View on a phone. - Anything sized from the window width must size from
contentMaxWidthinstead. It equals the window width on a phone and the column on a tablet. A three-up photo grid that divides the window by three overflows the column on an iPad; that was the profile media grid's bug.
A one-task form takes the narrower form column. Sign-in, the welcome screen's actions, a
short form with one submit: formColumnStyle, layout.formWidth (480). A 48pt pill 720 wide
is a banner, not a button, and a password field the width of the screen asks for a paragraph. A
long form with sections (contact details, an incident report) stays in the readable column.
The hero becomes a contained block. On a phone the Hero
cancels the page gutter and runs to the screen edges. In the column it can only reach the
column's edges, and a square-cornered slab that stops short of the screen reads as a mistake —
so at tablet widths it takes radius.card and reads as the block it now is. The component does
this itself; a screen does nothing.
Type does not scale up. A tablet is held closer than a whiteboard and further than a phone, and the scale already reaches ~46pt with Dynamic Type. Same tokens, same sizes, same 44pt targets. More room is spent on columns and air, never on bigger words.
The feed keeps its own column. Social's feed is capped at layout.feedColumnMaxWidth (600)
on every size class — a feed is narrower than a reading column on purpose, the way every feed is.
Its top bar rides the same 600 column, so nothing there changes on a tablet.
Grids add columns, never stretch tiles
A tile grid (see tile grid) on a tablet runs in the wider
gridColumnStyle — layout.gridWidth (1000) — and gains columns as it gains room. The tile
stays near its 104pt basis; a 390pt-phone tile stretched to 260pt is a banner with a name in the
corner, and the whole point of the grid is seeing everyone at once. Class Mode's check-in grid
already works this way: it measures its own width and checkInColumns() decides how many fit.
The same holds for a two-up layout of cards: two per row stays two per row inside the column. Do not turn a phone's two-up into a tablet's five-up of cards carrying sentences.
Two panes, at wide only
<SplitView> puts a list and the record it opens
side by side: the list between layout.splitListMinWidth (360) and layout.splitListMaxWidth
(400), the detail taking the rest, a border.hairline between them. Below breakpoint.wide it
renders the list alone and the row pushes, exactly as on a phone.
Only at wide. In iPad portrait (820) a 360pt list leaves the detail 460pt — narrower than the phone's detail once its own gutters are in, and every record would read cramped. Mail, Notes and Settings drop to one pane in portrait for the same reason.
When to split. When people move between records of one list, and seeing the list while reading one is the point: the roster and a student, messages and a conversation, a class's sessions and one session. Not for a tab home — a tab home is an index or a hero, and its rows open different kinds of thing. Not for a list whose rows open a modal task: a modal is a detour (see navigation), and it covers both panes.
How. useSplitView() answers "is the detail beside the list right now?". A row's onPress
selects when it is, and pushes when it is not. Selection is the caller's state; the selected row
is a selected state (red is legal there, per the spine). With nothing selected, the detail pane
says what goes there in one plain sentence, left-aligned — never a centred illustration in a
void. A deep link or a push notification still lands on the pushed detail screen, with a working
back, on every size class.
Rotation does not lose the place. When a screen drops from two panes to one with a record
selected — rotating to portrait, or a Split View narrowing — the screen pushes that record, so the
person is still looking at what they were looking at. That is the caller's job; SplitView only
lays out.
The tab bar stays at the bottom
On a tablet, Konjo keeps its bottom tab bar. It does not grow a side rail.
- The shape is the same on both. Navigation is four base tabs plus a conditional Teach, each owning a stack. A rail would carry the same five destinations with nothing to add, so it would change where things are without changing what there is.
- One body memory. An instructor uses their phone and the dojo's iPad in the same evening. The thumb that finds Teach at the bottom of the phone finds it at the bottom of the iPad, which matters most at the front desk, mid-class, with one free hand.
- The column needs the width more. A rail costs 80–100pt of a portrait iPad's 820, and the column is what makes a tablet read well. At the bottom, the bar costs height, which a scroll has plenty of.
- The desk is Studio's. The owner who wants a dense, rail-driven desktop has Konjo Studio, which has a rail (studio/metrics) because it has a dozen destinations. The app does not need to become a second Studio on a large screen.
Revisit only if the app grows past five top-level destinations, or a tablet-first role (a front-desk kiosk mode) needs navigation the phone does not have.
Rotation
The layout reads the window live, so every screen that adopts the column is ready to rotate.
The app is still locked to portrait in app.json, so today an iPad shows portrait only.
Unlocking it — tablet orientations on, phones still portrait — is a native configuration change
that goes through the release gate, and it should
ship once the remaining high-traffic screens have their column.
Never
- A phone layout that moves when a tablet layout is added.
- A screen comparing the window width to a number it typed itself.
- Branching on the device model instead of the window width.
- A grid that stretches its tiles instead of adding columns.
- A row 1,180 points wide.
- Two panes in iPad portrait.
- A side rail on the app's tablet layout.
- Bigger type because the screen is bigger.