Home/Blog

wontfit: Find the Element That Breaks Your Mobile Layout

wontfit shows your app at phone widths side by side, names the element that overflows and by how many pixels, and fails CI when a layout breaks.

wontfit: Find the Element That Breaks Your Mobile Layout

wontfit: Find the Element That Breaks Your Mobile Layout

A page can look finished on a laptop and still be broken on most of the phones that will open it. The failure is almost always the same: one element is wider than the screen, the whole page scrolls sideways, and nobody on the team noticed because nobody on the team develops at 375 pixels wide.

We released wontfit today as open source to catch that before it ships. It shows your running app at phone and tablet widths side by side, in the browser you already use, and when something overflows it names the element and says by how much. The same checks run headlessly, so a layout regression can fail a pull request the way a broken test does.

pipx install wontfit
wontfit --upstream http://localhost:3000 --pages /,/pricing --open

Why "it overflows" is not enough

Every mobile layout review starts with the same observation: the page scrolls horizontally at phone width. That tells you something is wrong and nothing about what. The usual next step is opening DevTools, switching to device mode, and deleting elements one by one until the scrollbar disappears.

That loop is slow for three reasons:

  • One page, one width at a time. A real review is three or four pages at three widths, repeated after every change.
  • The culprit is rarely where the symptom is. A pricing table with a min-width pushes the header, the footer and every section wider. The visible damage is everywhere; the cause is one element.
  • Nothing stops it coming back. A fix that nobody checks again regresses the next time someone adds a column.

AI-assisted development makes this more common, not less. Generated components tend to be tested at the width they were generated at, and a hard-coded width on an image or table is exactly the kind of detail a model gets plausibly wrong.

What wontfit shows you

wontfit is a small reverse proxy on 127.0.0.1 with a harness page in front of it. You point it at the dev server you already run; it serves your pages inside frames at the widths you ask for, one column per page and width.

Each frame reports four things:

CheckWhat it flags
Horizontal overflowThe outermost element wider than the frame, with its selector and bounds
Tap targetsInteractive elements smaller than 44 by 44 CSS pixels (WCAG 2.5.5)
Small textText rendered under 12px
InspectHover an element in one frame to highlight it, with its size, in every frame

The overflow check is the one that saves time. It walks the frame's DOM for elements that extend past the viewport and reports only the outermost ones, so a wide table is reported once as table.compare rather than as every tr and th inside it.

Most apps block being framed with X-Frame-Options or a CSP frame-ancestors directive, which is why browser-based preview tools often show a blank box. The proxy removes those two protections, and only those two: the rest of the Content Security Policy is forwarded unchanged. Because it strips framing protection, it binds to loopback only and has no option to listen anywhere else.

A real run

wontfit ships with a demo product site, Ledgerly, that refuses to be framed and has six planted mobile bugs. Running the headless check against it at iPhone SE width:

$ wontfit check --pages /,/pricing,/terms --widths se --fail-on overflow
 
page      size     overflow                small taps  text<12px
--------  -------  ----------------------  ----------  ---------
/         375x800  +401px (img.shot)       9           1
/pricing  375x800  +583px (table.compare)  3           1
/terms    375x800  fits                    3           10
 
FAIL (overflow):
  / @ 375: overflow +401px (img.shot)
  /pricing @ 375: overflow +583px (table.compare)

The process exits with status 1. Two pages overflow and each names its culprit: a screenshot with a fixed 760px width on the home page, and a comparison table with a min-width and no scroll wrapper on pricing. The terms page fits but has ten runs of text under 12px, which is reported without failing the build, because --fail-on was set to overflow only.

That split is deliberate. Overflow is almost always a bug. Small tap targets and small text are often judgement calls (an inline link in a paragraph is a small tap target by definition), so they are counted and shown, and you opt in to failing on them.

Gate the pull request

wontfit check needs Playwright, which is an optional extra so the core stays dependency-free:

pip install 'wontfit[shoot]'
playwright install --with-deps chromium
 
wontfit check --pages /,/pricing --widths se,iphone15,pixel8 \
  --fail-on overflow --json wontfit-report.json
wontfit shoot --pages /,/pricing --widths se,iphone15,pixel8 --out shots

check writes a JSON report and exits non-zero on the findings you choose. shoot writes a PNG for every page and width plus a contact sheet that tiles them, which is the image you want attached to a pull request that touches layout. The docs include a complete GitHub Actions job that starts your app, runs the check and uploads both as artifacts, and a pre-push hook for teams that prefer to catch it locally.

If the flags you pass are always the same, wontfit init writes a wontfit.toml you commit to the repository, and every contributor gets the same pages and widths by running wontfit with no arguments.

What it deliberately does not do

A tool that sits between a browser and an app should be explicit about its limits:

  • No WebSockets. Upgrade requests get a 501. Your framework's hot reload socket will not work through the proxy; the recipes show how to point Vite, Next.js, Nuxt and others at the dev server directly, and wontfit's own live reload (by URL hash or file watching) covers the rest.
  • No body rewriting. HTML, JavaScript and JSON pass through byte for byte. Only headers change.
  • No network exposure. It binds to 127.0.0.1. Do not publish its port from a container or through a tunnel; it removes security headers from whatever it proxies.
  • No device emulation. It sets widths. It does not emulate touch, user agents or device pixel ratios. If you need synced scrolling or device bezels, a dedicated tool like Responsively or Polypane does that well.

How it fits with the rest of your checks

wontfit does not replace visual regression testing or real-device testing. It covers the cheapest, most common class of mobile bug, content wider than the screen, early enough that fixing it is a one-line CSS change. Our view is the same as for security and performance: a check that runs on every pull request beats a thorough review that happens once before launch. We make the same argument about testing strategy and accessibility.

wontfit is MIT licensed, needs Python 3.9 or later, and has no runtime dependencies. The source is on GitHub, the package is on PyPI, and bug reports are welcome. If your app has layout problems bigger than one overflowing table, talk to us.