Let an AI agent check your site at phone width with a 30-line browser script
An AI cannot see a screen, but it can measure one: a short script loads each page in a 375px iframe and names what overflows, which headings spill, which labels collide.
- Mobile overflow is a number; scrollWidth and getBoundingClientRect say more than a glance
- A 375px iframe on the dev server checks dozens of pages in both languages in one loop, no clicking through
- Most culprits are long inline code, tabs in code blocks and wide heading fonts, each fixed with a line or two of CSS
- The script cannot judge "does it look right", so it ends with one screenshot for a human to glance at
One rule of this site is that no UI change counts as done until it has been checked in the browser at 375px wide, in both languages. The problem is that most code changes here are made by an AI coding agent, which has no eyes, and the human who does have eyes forgets. This article describes the method really used on 6amdev.com and four sister sites: replace “look” with “measure”, using about 30 lines of JavaScript run in the browser, and let the agent run it before every deploy.
Tested with current Chromium (October 2026) against an Astro dev server (npm run dev on localhost:4321). It works for any static site served from your own machine.
The problem: the AI cannot see, the human forgets
An agent can edit CSS and write Markdown, but ask it “does this page overflow on a phone?” and it answers from reading code, not from the real page. The answer is usually “it should be fine”, which is worth nothing.
Humans are not much better. This site has nearly thirty articles in two languages, plus category pages, series pages and the home page. One changed line in global.css can break any of them. Nobody really opens every page every time.
A page broken on a phone usually breaks quietly. It loads, the text is readable, it just scrolls sideways a little. The maker never notices; the reader notices at once.
The idea: measure instead of look
Almost everything the eye looks for on a phone translates into a number:
- Does the page scroll sideways?
documentElement.scrollWidthgreater thanclientWidth. - Which element overflows? Anything whose
getBoundingClientRect().rightpasses the viewport edge. - Does a heading spill?
h1,h2and the phrases we forbid from wrapping (classnw, used for loanwords in Thai) whose right edge passes the viewport. - Does a label in an SVG diagram hit a box?
getBBox()of each<text>compared with the<rect>it sits inside.
What remains is checking many pages without opening each one. The trick is a hidden <iframe>, 375px wide, one per URL: wait for it to load, then read iframe.contentDocument. That works because the dev server is the same origin as the page you are standing on.
The script, part by part
One function takes a list of URLs and returns a table of results. Run it from the console on any page of the dev server, or let the AI agent run it through a browser tool that can execute JavaScript on a page.
// Run on any page of the dev server (same origin). Returns one row per URL.
async function checkMobile(urls, width = 375) {
const rows = [];
for (const url of urls) {
// 1. Load the page in a hidden, phone-wide iframe and wait for it
const f = document.createElement('iframe');
f.style.cssText = `position:fixed;left:-9999px;width:${width}px;height:800px`;
f.src = url;
document.body.appendChild(f);
await new Promise(r => (f.onload = r));
const d = f.contentDocument, W = d.documentElement.clientWidth, problems = [];
// 2. How many px the page scrolls sideways
const overflow = Math.max(0, d.documentElement.scrollWidth - W);
// 3. Culprits: every element past the right edge, skipping pre (code blocks scroll on purpose), deepest only
const wide = [...d.querySelectorAll('body *')].filter(e =>
!e.closest('pre') && e.getBoundingClientRect().right > W + 1);
wide.filter(e => !wide.some(c => c !== e && e.contains(c)))
.forEach(e => problems.push(`${e.tagName.toLowerCase()}: ${e.textContent.trim().slice(0, 50)}`));
// 4. Headings and no-wrap phrases that spill
d.querySelectorAll('h1, h2, .nw').forEach(e => {
if (e.getBoundingClientRect().right > W + 1) problems.push(`heading: ${e.textContent.trim().slice(0, 50)}`);
});
// 5. SVG diagrams: text outside the viewBox, or wider than the box it sits in (2px margin)
const svgs = d.querySelectorAll('figure.diagram svg');
svgs.forEach(svg => {
const vb = svg.viewBox.baseVal, rects = [...svg.querySelectorAll('rect')].map(r => r.getBBox());
svg.querySelectorAll('text').forEach(t => {
const b = t.getBBox(), cx = b.x + b.width / 2, cy = b.y + b.height / 2;
const box = rects.find(r => cx >= r.x && cx <= r.x + r.width && cy >= r.y && cy <= r.y + r.height);
const outside = b.x < vb.x || b.x + b.width > vb.x + vb.width;
const wider = box && (b.x < box.x + 2 || b.x + b.width > box.x + box.width - 2);
if (outside || wider) problems.push(`svg: ${t.textContent}`);
});
});
// 6. One row per page, then clean up
rows.push({ url, overflow, diagrams: svgs.length, problems });
f.remove();
}
return rows;
}
Call it for both languages in one loop and print a table:
const slugs = ['security-headers-nginx', 'money-never-float', 'roblox-ep4-complete-obby'];
const urls = ['/', '/en/', ...slugs.flatMap(s => [`/blog/${s}/`, `/en/blog/${s}/`])];
console.table(await checkMobile(urls));
Things worth knowing about each part:
Part 1: the iframe is hidden with left:-9999px, not display:none. Elements inside a display:none frame have no size, so there is nothing to measure.
Part 2: clientWidth of documentElement can be a little under 375 when there is a vertical scrollbar, which is why the real value is read instead of hard-coding 375.
Part 3: printing every overflowing element gives you main, article, p and code nested four deep for one problem. Keeping only elements with no overflowing child leaves the real culprit. pre is skipped entirely because our code blocks already have overflow-x:auto and scroll inside themselves.
Part 5: find the rect that contains the centre of the text, then compare x ranges with a 2px margin. A label that sits in no box (one on an arrow, say) is checked only against the viewBox.
What it caught
Results from the last two weeks of real use. Every row was found before deploy.
| What the script reported | Cause | Fix |
|---|---|---|
A Roblox article 508px wide, code: otherPart.Parent:FindFirstChildWhichIsA("Humanoid") | long inline code could not wrap and pushed the whole page out by 133px | code { overflow-wrap: anywhere } for inline code only, not inside pre |
| Code blocks scrolling unusually far right | the Lua files use tabs, which browsers render 8 spaces wide | pre { tab-size: 4 } |
td: Humanoid …, a table overflowing | the table already had display:block; overflow-x:auto; the culprit was inline code inside a <strong> cell | went away with the first fix; the table was never touched |
heading: … on the English home page of a sister site | the hero headline uses a wide display font, and a long English word would not break | fixed once in the shared CSS, so every site got it |
svg: … in an obby path diagram | a label overlapped a box edge and an arrow, invisible at a glance | shift the text’s x in scripts/diagrams.py and regenerate |
Look at the third row. By eye you would blame the table and spend time on table CSS. The deepest overflowing element was code inside strong; fix inline code and the table stops overflowing on its own. That is what keeping only the deepest culprits buys you.
Several rows are problems you would “almost not see” on a big screen. A page 20px too wide is invisible on a desktop. The number is not.
What the script cannot judge
The script answers “does it overflow, does it collide”, never “does it look right”. Odd spacing, washed-out colours in the dark theme, buttons too small to tap, an image cropped so its meaning is lost: those still need a human eye.
Our routine is that the agent runs the script until the table is clean, then takes one screenshot per language at 375px and hands them over. The human’s job shrinks from “click through thirty pages” to “glance at two pictures”.
The other limit is that an iframe’s document is readable only from the same origin, so this works on our own dev server and nothing else. It cannot check someone else’s site, nor our live site, which sends X-Frame-Options: DENY. For a live page, open it directly and run parts 2 to 5 against the current document.
Try it in 10 minutes
- Start your site’s dev server and open any page in the browser.
- Open DevTools (F12), Console tab, paste the
checkMobilefunction above. - Call
console.table(await checkMobile(['/', '/about/']))with your own paths. - If
overflowis not 0, readproblemsand go fix that element directly. - If you use an AI coding agent with a browser tool, add a rule to the project’s instruction file: before calling any UI work done, run this function on every page touched and paste the table.
Step 5 is what changes the game. Instead of a person remembering to check, the agent checks every time because it is a project rule, and because the culprit is named in the table, it fixes it in the same pass.
Common mistakes
- Hiding the iframe with
display:none, getting zeros everywhere, and calling it a pass - Reading
contentDocumentbeforeonloadand gettingnullor an empty page - Checking only one language; English overflows more often because of long words and a wider display font
- Not skipping
pre, so every code block is reported as a problem when it is meant to scroll - Fixing the outer element (table, section) when the real overflow is inline code inside it
- Putting
overflow-x: hiddenonbodyto hide the symptom; the page stops scrolling, but the overflowing text is simply cut off - Running the script once when the site is finished and never again after adding articles, which is exactly when long inline code shows up
FAQ
Why not DevTools device mode or Lighthouse?
Both work, but someone has to open each page, and Lighthouse does not name the element that overflows. This script checks every URL in one loop and points at the culprit, so an AI agent can fix it in the same pass and repeat before every deploy.
Can I run it against another site, or my live site?
An iframe's document is readable only from the same origin, so it works on your own dev server. A live site with X-Frame-Options DENY cannot be embedded at all; open that page directly and run the measuring parts against the current document instead.
Do I need an AI for this?
No. Paste the function into the DevTools console and call it by hand. The payoff is letting a coding agent run it after every change and fix what it finds without anyone reminding it.
Commands and settings were tested on sample systems. Try them on a test machine before production.