Device previewer
A real page in a real frame at a real width. Media queries inside it evaluate against the frame, not your screen, so this is what a phone actually gets — not a scaled screenshot and not a container-query approximation. Resize it and watch the navigation collapse, the table become cards and the tab strip start scrolling.
320px is not a legacy width
It is the iPhone SE and every Android device in landscape-locked kiosk mode, and it is what a 390px phone becomes at 125% text zoom. If a layout survives 320, it survives.
320px — smallest phone still in use. Grids are single column, the nav is a burger, the hero stacks, tables scroll. 390px — iPhone 14/15. Same layout as 320 with more breathing room; nothing new engages. 430px — iPhone Pro Max. Still below every breakpoint: the framework is mobile-first, so this is the base stylesheet. 768px — tablet portrait. Two-column grids engage, the form rows split, the sidebar is still hidden. 1024px — small laptop. Three-column grids, sticky sidebars, the full navigation. Full width — four-column grids and the container's 1160px ceiling, which is a measure limit rather than a breakpoint.
Why an iframe and not a scaled screenshot
Media queries inside an iframe resolve against the iframe's own width, so a page framed at 320px behaves exactly as it would on a 320px phone. A CSS transform would shrink the picture without changing a single breakpoint, which is the trap most "device preview" widgets fall into. The cost is stated on the performance page: this site's policy allows same-origin framing, and refuses everything else.
Breakpoints
Two sets, and they exist for different reasons. The component breakpoints are where a layout genuinely stops working; the grid breakpoints are a conventional ladder for the column system.
| Width | Module | What changes |
|---|---|---|
| 620 | Content, Forms, Components | Stacking tables become cards, form rows collapse to one column, the tab strip scrolls, grids drop to a single column |
| 640 (height) | Core | Short landscape: section and hero rhythm is capped rather than scaled |
| 760 | Forms, Footers | Horizontal fields stack, footer columns collapse |
| 860 | Headers, Components | Navigation becomes the checkbox menu, bottom bar appears, changelog entries stack |
| 900 | Core | Shell drops its sidebar, three and four column grids become two |
| 1100 | Core | Four column grids become three |
| 576 / 768 / 992 / 1200 / 1400 | Grid, Space, Utilities | The sm, md, lg, xl and xxl infixes — mobile first, so each applies from that width up |
Nothing above is hardcoded twice. If you change where the navigation collapses, you change it in Headers and nowhere else, because no other module refers to 860px.
The two guards
Most horizontal-scroll bugs on a phone come from one of two things, and both have a one-line fix that is easy to leave out. The panes below are identical markup; the right-hand one has the guard removed.
min-width: 0 on grid children
A grid track is 1fr, but 1fr means at least the content's
minimum size. A wide child — a table, a long identifier, a <pre> —
pushes its track past the container and scrolls the page sideways.
Both panes clip at their own border here so the page itself stays usable — in a real layout the right-hand case pushes the whole document sideways. Core applies the guard to .pf-grid, .pf-split, .pf-shell and every .pf-row child.
A measure on body copy
Line length is not a stylistic preference at wide widths; past roughly 80 characters the
eye starts losing its place returning to the next line. Core caps p and
li at --pf-measure, 68ch.
Reading a long line is easy enough at the start, but the return sweep to the beginning of the next line is where the eye gets lost, and a measure is what keeps that sweep short enough to land in the right place every time.
Reading a long line is easy enough at the start, but the return sweep to the beginning of the next line is where the eye gets lost, and a measure is what keeps that sweep short enough to land in the right place every time.
Opt out per element with .pf-measure-none, which is what card and alert bodies do — they are already narrow.
Tables on a phone
Two strategies, and choosing wrongly is worse than either.
Scroll — the default
The wrapper scrolls horizontally and columns keep their alignment. Right for comparison tables, where reading across is the point.
.pf-table-wrap
Stack — opt in
Each row becomes a card with its column names inline. Right for lists of records, wrong for anything you compare across, because stacking destroys the alignment that made it a table.
.pf-table--stack
Capability, not just width
Width is the least interesting thing about a device. Six other queries do more work in this framework than the breakpoints do.
| Query | Why it matters more than width |
|---|---|
pointer: coarse | A tablet is desktop-width with finger targets. Raises hit areas to 44px and floors fields at 16px so iOS does not zoom. |
hover: hover | Hover affordances only exist where a pointer can hover. On touch they are a state you can only reach by pressing and holding. |
prefers-reduced-motion | Vestibular triggers are not a screen size. Every transition drops to 0.01ms. |
prefers-color-scheme | The whole palette flips, unless the page pins a theme. |
forced-colors | Windows High Contrast replaces every colour; backgrounds vanish and borders have to be reinstated. |
@container | A card in a sidebar and the same card in a main column are the same width problem. A viewport query cannot tell them apart. |
The dvh detail
On a phone 100vh is the viewport with the address bar hidden — taller
than what you can see, so a "full height" element overflows by the height of the bar.
Every full-height rule here declares vh first and dvh second, so
older engines take the first and newer ones the second.
Next: what the framework handles for you, and what it cannot.