---
title: "Layout"
description: "The page shell, the safe-area and keyboard contract, and the two screen shapes that between them cover almost every consumer health surface."
url: "https://opsinjs.pensievelabs.org/foundations/layout"
source: "https://opsinjs.pensievelabs.org/foundations/layout.md"
section: "Foundations"
kind: "foundation"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["page shell", "safe area", "app shell", "thumb zone"]
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> Nothing is missing from this page. The data simply does not live in
> the prose.

<PageTemplate kind="foundation" />

## Overview [#overview]

Layout in opsinjs is deliberately small. There is no grid system, no twelve
columns, no layout component zoo. A consumer health app is, structurally, a
scrolling column with a persistent action, on a device held in one hand. Almost
everything that goes wrong with its layout goes wrong at the edges: under the
notch, behind the home indicator, beneath the on-screen keyboard, or above the
fold on a small phone where the one thing the reader came for is the one thing
they cannot see.

So this page specifies the shell, the edges, and two screen shapes. Everything
about how the shell *changes* with available width belongs to
[Responsive modes](./space/responsive-modes.mdx), and everything about how much
room things get belongs to [Space](./space/index.mdx).

The thing this is most often confused with is a page template. A template
prescribes content. A shell prescribes the frame, the insets and the ordering
rules. What content goes in it is a [pattern](../patterns/index.mdx) or a
[screen](../screens/index.mdx).

## How it works [#how-it-works]

**The shell has four regions**, and their order in the DOM is the order a screen
reader meets them, which is not always the order they appear on screen:

1. **Header** has the screen's name and at most one secondary action. It is a
   heading, not a picture of one.
2. **Content** is a single scrolling column. Multiple scroll containers on one
   screen is the most reliable way to lose somebody using a keyboard or a
   switch.
3. **Action** is the persistent primary action, if the screen has one. It is
   the last thing in the DOM and it is pinned visually, so tab order reaches it
   after the content rather than before it.
4. **Chrome** is the tab bar or equivalent, outside the screen's landmark
   entirely.

**The edges are a contract, not a per-screen decision.** Every one of the four
regions resolves its outer padding from a token that already includes the
platform inset, so a component never writes `env(safe-area-inset-bottom)` itself
and a screen never ends up with a dismiss control under the home indicator. The
same applies to the on-screen keyboard: the action region and any focused field
sit above the keyboard inset, so typing a reading never hides the field you are
typing it into.

**Reach beats symmetry.** On a phone, the bottom third of the screen is where a
thumb comfortably lands and the top corners are where it does not. Primary
actions go low; destructive and irreversible ones do not go where a thumb rests
by default. This is opinion informed by the platform guidance rather than a
measured result, and it is stated as such.

**Two shapes cover most screens.**

*The **overview** shell.* One heading, then a vertical run of tiles or cards, one
per category or per metric, then a persistent action. Its rules: every tile
carries its own status and its own freshness; there is no sort order that mixes
the two axes; and the screen has a stated escalation budget, because an overview
is where alarm fatigue is manufactured. See
[Alarm fatigue](../health/alarm-fatigue.mdx).

*The **reading** shell.* One value at the top with its unit, its status and its
range; then what it means; then what to do; then history. Its rules: the value is
above the fold on the smallest supported screen, nothing may cover it (see
[Layering](./layering-and-elevation.mdx)), and the *meaning* precedes the *action*
because a reader who is told what to do before they are told what it is will
usually do neither.

## Using it [#using-it]

**Do not invent a third shell without writing down why.** Most requests for one
are actually a request for a different arrangement of content inside the reading
shell. If you genuinely need one, it belongs in
[Screens](../screens/index.mdx) with a spec, not as a local layout.

**Order for the reader, not for the designer.**

<DoDont>
  <DoDont.Do>
    Reading shell: value → status → range → plain-English meaning → what to do →
    when it was taken → history. Somebody who stops reading after two lines has
    still got the two lines that matter most.
  </DoDont.Do>

  <DoDont.Dont>
    Chart first, value below it. The chart is the most visually interesting part
    of the screen and the least useful thing to a person who wants to know if
    today's number is a problem. Put the number first and the chart under it.
  </DoDont.Dont>
</DoDont>

**One scroll container, one landmark set, one H1.** The header region is the H1;
sections inside content are H2. A screen with two H1s is a screen that will be
described wrongly by every tool that summarises it, including the ones your
readers use.

**Content has a maximum measure, even on a large screen.** Wide screens get more
of the story visible at once, not wider paragraphs. The rule and its mechanism
are on [Responsive modes](./space/responsive-modes.mdx).

## Tokens [#tokens]

Layout consumes the space scale, the safe-area insets and the container
thresholds; it owns no scale of its own. The shell's outer padding, the action
region's inset and the keyboard-avoidance offset are generated from
`tokens/space.json` by `scripts/build-tokens.mts`.

<TokenTable scope="space" />

## Accessibility impact [#accessibility-impact]

Layout is where document structure and visual arrangement can silently disagree,
and every item below is a real failure that passes a visual review.

* **DOM order is reading order.** The pinned action region is last in the DOM
  even though it is visually low on screen, so tab order and screen-reader order
  both reach it after the content it applies to. Any layout technique that
  reorders content visually without reordering it in the DOM breaks this.
* **Landmarks are not decoration.** Header, main, and the chrome region are
  distinct landmarks with distinct names, which is how somebody using a screen
  reader skips to the content without listening to the tab bar first.
* **Safe areas are an accessibility requirement.** A control under the home
  indicator or behind the keyboard is not merely inconvenient; for somebody with
  limited dexterity it is unreachable, and for somebody using a screen reader it
  is reachable but described in the wrong place.
* **Reflow.** The shell must survive 400% zoom without a second scroll axis, and
  must survive a 1.3× text-size multiplier with no clipping at all. The checks
  are on [Text resizing and zoom](../accessibility/text-resizing-and-zoom.mdx)
  and [Dynamic Type](./typography/dynamic-type.mdx).
* **Sticky regions must not eat the viewport.** A sticky header plus a sticky
  action bar on a small phone at a large text size can leave almost no content
  visible. Both collapse under the compact mode rules rather than persisting at
  full height.

## Related [#related]

* [Responsive modes](./space/responsive-modes.mdx) shows how this shell behaves as
  the container grows, and why the rule is "more of the story", not "bigger".
* [Layering and elevation](./layering-and-elevation.mdx) says what may sit over
  the shell, and the rule that nothing may cover the value.
* [Screens](../screens/index.mdx) covers whole-screen specimens built from these
  two shapes.
