"The app is responsive" is a claim, and for months I was making it the way most teams do: by resizing my own browser window on the pages I happened to be working on. Then a beta tester opened a page on a narrow phone and sent a screenshot of a button living half outside the screen. The claim needed to become a property — something verified, not vibed. This entry is the methodology that got us there, and it applies to any web app regardless of stack.
Decide what "broken" means, precisely
You can't audit against taste. The first real step was writing down the violation classes — the specific, checkable ways a layout fails a human:
- Horizontal overflow: the page scrolls sideways, or content is clipped by the viewport edge.
- Unusable controls: interactive elements that overlap each other, get covered by fixed overlays, or shrink below a minimum touch-target size (44px is the floor we enforce).
- Unreadable text: body copy below a readable floor, or truncation that eats meaning rather than trailing decoration.
- Layer collisions: banners, modals and floating buttons colliding with each other or with content they were never tested against.
Each class is mechanically checkable from a rendered page, which is what makes everything after this point automatable.
Sweep the matrix, count everything
The audit itself is a matrix: every route in the app crossed with a ladder of viewport widths from 360 (small Android) up through 390, 768, 1024, 1440 and 1920. A headless browser drives each combination — logged in, real data, real overlays — and evaluates the violation checks against the live DOM, flagging every element that trips one. By the end, ours was hundreds of route-viewport sweeps across a couple dozen routes, and the number that matters is the last one: the final full pass recorded zero violations. Not "looked fine" — counted zero. The distance between those two phrases is the entire point of the exercise.
Fix by layer, not by page
The audit's most valuable output wasn't the bug list — it was the realization that the bugs clustered. Page after page failed for the same handful of upstream reasons: a shared container that didn't constrain width, a settings-row primitive that couldn't wrap, floating UI elements that each assumed they were the only one in the corner. Fixing those meant fixing the component layer, and dozens of per-page symptoms evaporated at once. If you audit and then fix page-by-page, you're mopping around a running tap: trace each violation to the primitive that caused it, fix it there, and re-sweep to watch the class disappear globally.
Skeletons deserve the same scrutiny
A subtle class of failure only showed up under systematic sweeps: loading states. A page can be perfectly responsive after data arrives and still jump, flash or overflow during the second before — skeletons sized for desktop, placeholders that don't reserve the right space, promo panels that render before the query gating them resolves. The rule that came out of it: loading UI must mirror the arrival state, at every width. Users spend a meaningful fraction of every session looking at your loading states; they're pages too.
Emulation finds math; devices find truth
A headless sweep at 390 pixels wide catches layout arithmetic — overflow, collision, target size. It does not catch what a real phone does to your fixed-position elements when the browser toolbar collapses, how a notch's safe-area inset eats a corner button, or the way a real touch keyboard reflows a form the emulator swore was fine. The workflow that stuck: automate the matrix for every change, and walk the critical flows on a couple of physical devices before anything ships to testers. The sweep keeps regressions out; the devices keep the sweep honest.
Keep it fixed: doctrine plus regression
A one-time audit decays the moment the next feature merges. Two things keep the property true. First, the violation classes and layout rules got written into the project's standing engineering docs — new UI is built against them, not re-litigated per PR. Second, the sweep runs as a repeatable test command, so a layout regression is a failing check with a route and a width attached — not a screenshot from a disappointed tester three weeks later. Like most quality work on this project, the AI pair executes the sweeps tirelessly, and the human decides what the rules are. That division of labour is the only reason a one-person team can afford properties instead of claims.