Responsive
The rest of the foundations describe values that do not change with viewport width. This page describes the four points at which the layout does.
Breakpoints
Four breakpoints, declared in
brand/tokens/variables.css and
normative here:
| Name | Min width | The layout it describes |
|---|---|---|
sm | 640px | Large phone, landscape phone |
md | 768px | Small tablet — the first two-column layout |
lg | 1024px | Tablet landscape, small laptop — sidebars appear |
xl | 1280px | Desktop — the 1100px measure is fully margined |
Below sm is not a breakpoint; it is the base. Write the narrow layout
first and add width with min-width queries, so an unstyled or unsupported
viewport gets the single-column layout rather than a clipped desktop one.
/* Base — single column, 360px and up. */
.panel-row { display: grid; gap: var(--space-4); }
@media (min-width: 768px) {
.panel-row { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.panel-row { grid-template-columns: 1.5fr 1fr; }
}
1 col
1 col
2 col
+ sidebar
margined
How the grid collapses
The grid is 12 columns inside the 1100px measure. It does not stay twelve columns all the way down — it resolves to four column counts:
| From | Columns | Gutter | Page padding |
|---|---|---|---|
| base | 4 | 16px | 16px |
md 768px | 8 | 16px | 24px |
lg 1024px | 12 | 24px | 32px |
xl 1280px | 12 | 24px | auto — the measure caps at 1100px |
Below md, every multi-column region becomes one column. There is no
two-column layout on a phone: two 160px columns are two unreadable columns.
Regions stack in source order, so the DOM must already be in reading order
— do not rely on order or grid-area to fix a sequence that is wrong in the
markup, because that breaks the focus and reading
order.
Spacing steps down one rung when the grid collapses: a --space-6 (32px)
section gap on desktop becomes --space-5 (24px) below md. Radius, type
scale, and border weights do not change — a card is a 9px card at every
width.
Wider viewports gain margin, not line length. At xl the content stays 1100px
wide and centres; it does not grow to fill a 1920px display. This is the same
rule the spacing page states, restated here because it is the most common thing
a responsive rewrite breaks.
Touch targets
44×44px minimum for anything tappable, at every viewport — the floor is a finger, not a breakpoint.
The default control heights are 32 / 40 / 48px, so the sm and md sizes are
below the floor on their own. Two ways to resolve it, in order of
preference:
- Grow the hit area, not the control. Keep the 40px visual button and give it a transparent 2px vertical extension, or wrap it in a 44px-tall row. The measurement stays normative; the target clears the floor.
- Use
lg(48px) on touch-primary surfaces. Correct for the primary action on a phone, where a 40px button next to a 48px one reads as demoted.
Never shrink to sm (32px) for a touch target. Spacing between adjacent targets
is at least --space-2 (8px), so a mis-tap does not fire the neighbour.
/* Preferred: 40px control, 44px target. */
.pj-btn--md {
min-height: 40px;
position: relative;
}
.pj-btn--md::after {
content: "";
position: absolute;
inset: -2px 0; /* 40 + 2 + 2 = 44 */
}
Mobile navigation
Use a bottom tab bar for application surfaces. Use a top drawer for documentation and marketing surfaces. The choice follows the shape of the navigation, not the shape of the device:
- An application has a small, flat set of destinations — the dashboard example has five. Five or fewer flat destinations fit a tab bar, which keeps the current location permanently visible, sits in the thumb arc, and costs no taps to reach. That is worth the 56px of permanent screen it occupies.
- Documentation has a deep, nested tree. A tab bar cannot express it, and flattening the tree to fit one is worse than a drawer. A drawer behind a labelled disclosure control costs one tap and can show the whole hierarchy.
The failure mode is a tab bar with six or more items, or one that hides the primary destination behind “More”. If the destinations do not fit, the surface is a drawer surface.
Both patterns carry the same obligations: the current destination is marked with
an accent underline or fill plus the label, never colour alone; the drawer
traps focus while open, closes on Esc, and returns focus to its trigger — the
same contract as a modal.
Tables, code, and diagrams
Three content types cannot reflow, and each has one answer:
- Tables scroll horizontally inside their own container with the first column pinned — never the page. See wide tables.
- Code blocks scroll horizontally. Do not soft-wrap code: a wrapped line changes what the code appears to say.
- Diagrams get a scrollable container or a re-drawn narrow variant. Do not scale a diagram down until its labels are unreadable — an 8px label is not a responsive diagram, it is a broken one.
Worked example
Mobile onboarding —
three screens in a device frame: an onboarding panel on midnight, an engagement
list of stacked cards, and a finding-review screen with two side-by-side
actions. It is the dashboard
below md: same tokens, same components, one column, tab bar instead of
sidebar.
Write the base layout first and add width with min-width queries. Put regions
in the DOM in reading order. Give every tappable thing a 44px target.
Keep two columns below 768px, reorder regions with CSS to fix source order, scale a diagram until its labels are illegible, or let a wide table scroll the whole page.