<!-- https://getkonjo.com/design/foundations/layout · source: docs/design/foundations/layout.md -->

# Adaptive layout

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](../konjo-design-language.md). The laws in the spine apply here unless this chapter contradicts them.

Kata decided that **every screen has a designed tablet layout**
([the interview](../2026-09-29-kata-founder-interview.md)). 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()`](../../../src/theme/useLayout.ts)**, 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 `columnStyle` into its `contentContainerStyle`, after the screen's own
  style. It is `undefined` on a phone, so the phone's style is untouched. Do not wrap the
  scroll's children in a View instead — a `gap` on 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.
  `ScreenHeader` does this itself; a `ChildHeader` takes `style={columnStyle}`; a hand-drawn
  header row spreads `columnStyle` into its style.
- **Anything else outside the scroll** — a search row, filter chips, a footer — goes in
  [`<ScreenContainer>`](../../../src/components/ui/ScreenContainer.tsx), which is a bare View on a
  phone.
- **Anything sized from the window width** must size from `contentMaxWidth` instead. 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`](../../../src/components/ui/Hero.tsx)
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](../patterns/tile-grid.md)) 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>`](../../../src/components/ui/SplitView.tsx) 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](../patterns/navigation.md)), 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](../patterns/navigation.md) 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](../studio/metrics.md#the-rail)) 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](../../../.claude/skills/release-gate/SKILL.md), 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.
