responsive-overflow-tests
v0.5.0
Published
Framework-agnostic Playwright tests that catch horizontal-overflow layout breaks across Tailwind breakpoints and real-device viewports — no screenshots, no AI.
Maintainers
Readme
responsive-overflow-tests
Automated tests that catch horizontal overflow — a page that ends up wider than the screen showing it, so the whole thing scrolls sideways.
You give it a list of routes. It loads each one in a real browser at every screen width that matters, measures whether anything crosses the right edge of the viewport, and fails with the element responsible.
✘ no horizontal overflow — /about @ md-768 (768px)
Horizontal overflow at /about @ md-768 (768px):
scrollWidth 812px > clientWidth 768px (44px over, tolerance 1px).
Worst offending elements:
div#hero.grid.grid-cols-3 — right edge: 812px, width: 812pxSetup is one config file. Runs in seconds, in npm test or CI.
Why
Horizontal overflow is the most common responsive bug there is, and one of the
hardest to notice. A single element too wide — an image without a max-width, a
table, a long unbroken URL, a grid that doesn't wrap, a 100vw block inside
padding — drags the entire page with it. Nothing errors. It renders fine on a
desktop monitor, so it survives development and code review, and usually
surfaces after release.
It also isn't only cosmetic:
- Mobile is the version Google indexes. Under mobile-first indexing, the mobile rendering is what gets crawled and evaluated.
- It's an accessibility failure. WCAG 2.1 criterion 1.4.10 (Reflow, level AA) requires content to reflow to a 320px width without horizontal scrolling.
- It costs conversions. A submit button sitting off the right edge is not a styling nit.
Checking it by hand means opening every page at a dozen widths after every change, which nobody sustains. This makes it a test: no screenshots, no baselines, no diffs to approve — it passes, or it names the element to fix.
For AI coding agents
An agent editing layout code can't see the result, so it reports the work done and the break ships. Ask it to check and it writes a throwaway Playwright script with viewports it picked that session — different next time, so nothing is ever comparable.
This is that check, settled once: the same viewports in every project, in a
config you can read. light is 7 viewports and prints a line or two, cheap
enough to run after every edit. medium is for real layout changes, full for
CI.
Step 7 adds it to your agent's instructions in one paste.
Framework and language agnostic
It drives a browser over HTTP, so it works against Next, Astro, Vite, SvelteKit, Nuxt, Remix, Laravel, Rails, Django, WordPress, or a folder of static HTML. It never reads or touches your source — only the rendered page.
The default viewports cover both Tailwind v4 and Bootstrap 5 breakpoints, so neither stack is the assumed one. On any other system, replace them with your own in one config key.
Why not visual/screenshot testing?
Screenshot tools compare pixels, so someone has to decide whether a diff is a
real break or just a font rendering slightly differently. This compares
scrollWidth to clientWidth instead. Wider means something overflowed — no
baselines, no review step, no flaky diffs. On failure it names the elements
crossing the right edge.
One break it cannot see: if an ancestor has overflow-x: hidden, content
wider than the page is clipped rather than scrolled, so scrollWidth never
moves and the check passes while text is cut off. Nothing in the DOM separates
that from a marquee, so it needs a screenshot pass. See
What a green run means.
Requirements
- Node 18+ — the tests run on Node even if your app doesn't.
- A site you can serve locally over HTTP (any stack).
Getting started
1. Install
npm install --save-dev responsive-overflow-tests @playwright/test@playwright/test is a peer dependency, so install it alongside.
2. Install a browser
Playwright ships the runner; the browser binary is a separate download.
npx playwright install chromium3. Scaffold
npx responsive-overflow-tests initThat creates three files and adds one line to your .gitignore:
| File | You edit it? | What it is |
| ------------------------------------- | ------------------ | ------------------------------------------- |
| responsive-overflow-tests.config.ts | Yes — this one | Your port, routes, everything |
| playwright.config.ts | No | Three-line shim that reads the config above |
| e2e/overflow.spec.ts | No | Generated test stub |
| .gitignore | — | Gains .playwright/ |
Nothing else is written, and existing files are never overwritten.
Not using TypeScript? init detects that and scaffolds .js/.mjs instead.
Already have a Playwright suite?
initsees yourplaywright.config.*, leaves it alone, and prints how to connect the two. Your config keeps owningbaseURL,webServer, projects and workers; this one contributes routes and viewports only. Delete the generated shim and follow Using an existing Playwright config — there's one Playwright gotcha there worth reading before you run it.
Gitignore. Every artifact a run produces — traces, error context, the cached login session — goes to
.playwright/, and nothing is written outside it.initadds that one line to your.gitignorefor you. Worth confirming it landed, particularly if your ignore rules live somewhere non-standard (a global gitignore,.git/info/exclude, or a monorepo root):.playwright/
4. Configure
Open responsive-overflow-tests.config.ts. To get a first run, there are three
things to set — where your site runs, how to start it, and what to check:
import { defineConfig } from "responsive-overflow-tests";
export default defineConfig({
// The port to serve the site on for tests.
// Framework defaults: Next 3000 · Astro 4321 · Vite/SvelteKit 5173 · Laravel 8000
port: 3000,
// How to boot it. Delete this line if you start the server yourself.
// Laravel: "php artisan serve"
// PHP: "php -S localhost:8000 -t public"
// Static: "npx http-server ./dist -p 8080"
startCommand: "npm run dev",
// The routes to check.
routes: {
light: ["/", "/about", "/contact"],
medium: [],
full: [],
},
});You don't need every route on your site. Put the pages whose layout actually
matters in light, and let the other tiers grow over time.
Give the tests their own port. The obvious choice is your framework's default, and it's the one that bites. Locally the runner attaches to an already-running server rather than booting one (
reuseExistingServer), so if your ownnpm run devis sitting on that port, the tests silently check that — possibly a different branch, possibly a stale build — and you get a green run against the wrong site. Pick a port you only use for tests, set it here and instartCommand, and the two can never collide.
That's the minimum. The same file also holds authentication, custom viewports, ignored selectors, timeouts, and tier defaults — see the full config reference. You will not need a second config file for any of it.
5. Run
npx playwright testPlaywright boots your server, checks every route at every viewport in the active tier, and shuts down. One test per route × viewport.
Testing a page that charges a card, sends mail, or writes to a database? These are real page loads in a real browser, and a checkout route will do checkout things. Read Routes that do something before you add one — especially if this will ever run in CI.
6. Wire it into your test script
{
"scripts": {
"test:responsive": "playwright test",
"test": "vitest run && npm run test:responsive"
}
}If this is the only Playwright suite in the project, that is all you need.
If you have other Playwright suites, narrow it to this one with
playwright test --project=responsive — the suite runs as a project of that
name. Use that rather than -g "horizontal overflow": matching on a test's
title works until someone renames a test, and then it silently matches nothing
and reports a pass.
7. Tell your AI agent about it
Do not skip this one. An agent that isn't told how your project tests will invent a way every session — a throwaway Playwright script with viewports it chose that morning, thrown away afterwards. Nothing is comparable between runs, and whether a break gets caught depends on who was driving.
The fix is to write the routine into your project's own instructions. Paste this to your agent once:
Read
node_modules/responsive-overflow-tests/ADVANCED.md, find the "AI agents & automated workflows" section, and add its## Testingblock to this project'sAGENTS.md(orCLAUDE.mdif that's what we use).
ADVANCED.md ships inside the package, so this needs no network access.
What your agent ends up with is a single ## Testing section covering both
halves of the job:
- Responsive overflow — run
lightwithout asking after any markup/CSS/ layout edit and before a commit;mediumon a significant layout change; neverfullunprompted, but offer it before merging tomain. Plus: a failure means fixing the element, not raisingtolerancePx. - Visual verification — render the touched routes and actually look, because a green overflow run is not evidence the page looks right.
See AI agents and automated workflows for the block itself and the reasoning behind the tiers.
Coverage tiers
One knob: tier. It scales both the viewports and the routes, and the tiers
are cumulative — medium runs everything in light too.
| Tier | Viewports | When to run it |
| ----------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------- |
| light (default) | 7 — every layout regime of Tailwind v4 and Bootstrap 5, plus 320px and 360px | After any markup/CSS/layout edit, and before a commit |
| medium | +11 — Bootstrap's own breakpoints, one px below every breakpoint, and real desktop widths | Significant layout changes, and on pre-push |
| full | +10 — real phone widths, remaining boundaries, fold, QHD, ultrawide | CI, and before merging to main |
Override per run:
RESPONSIVE_TIER=full npx playwright test320px — the width WCAG 1.4.10 Reflow is written against — is in light, so
every run checks it.
Why medium tests 767px and not just 768px
Tailwind and Bootstrap breakpoints are both min-width, so md engages at 768.
At exactly 768 you get the layout you designed and looked at. 767 is where
the smaller layout is still active and stretched to its widest — which is
where things actually burst, and the width nobody ever eyeballs. medium covers
both sides of every breakpoint in both systems for that reason.
Going further
All of it lives in the same single config file — see ADVANCED.md:
- Testing pages behind a login
- Routes that do something — payments, mail, side effects
- Custom viewports and resolutions
- Ignoring embeds you can't fix
- Continuous integration and git hooks
- AI agents and automated workflows
- Troubleshooting
- Full config reference
- Using an existing Playwright config
- Writing your own spec
What it does not do
- It does not check vertical overflow, visual styling, or content.
- It does not replace visual review — it proves the layout didn't physically break, not that it looks right.
- It does not need or produce baseline images.
- It does not check pages it isn't told about — routes are an explicit list, not a crawl.
Pair it with a screenshot pass — recommended
This is the deterministic half, and it is deliberately only half. It will never
tell you that two elements overlap, that a heading wrapped badly, or that a
section is unreadable at 390px — all of which fit inside the viewport and pass
here. It is also blind to a break hidden behind an ancestor's overflow-hidden,
which produces no scrollbar to measure.
The two are designed to work as a pair, and the recommended setup runs both.
If you use Claude Code,
frontend-screenshot-verification
renders a route across a tiered matrix of real device viewports so an agent can
review them. Any screenshot tool works — the point is that something has to
exercise judgment.
Write both into your project's instructions together, as one ## Testing
section — see step 7. Documenting only the
mechanical half is how an agent ends up improvising the visual half.
The two overlap slightly: that plugin also flags horizontal overflow, since it's
free once the page is loaded. Treat this package as the authoritative one — it
names the offending element, returns a non-zero exit code, and costs nothing to
run, so it's the one that belongs in npm test and CI. The screenshot pass is
for the questions no mechanical check can answer.
License
MIT © CyberPunk
