How to Fix Horizontal Scroll on Mobile: Find the Culprit

Fix horizontal scroll on mobile by finding the element wider than the screen: a DevTools snippet, 8 causes with CSS fixes, and overflow data from 188 sites.

BugViso

15 min read

To fix horizontal scroll on mobile, find the element that's wider than the viewport and fix its sizing, rather than hiding the overflow. Run a snippet in DevTools that lists every element whose right edge passes document.documentElement.clientWidth, keep the outermost one (its parent fits, it doesn't), and correct the cause: a fixed width, a negative margin, an absolutely positioned or off-canvas element, a long unbroken URL, an image without max-width, or a wide table or code block.

It's more common than it looks, and it depends on width. On 9 October 2026 we loaded 188 random homepages in mobile emulation. 5.9% scrolled sideways at 390px (an iPhone-sized screen), and that doubled to 12.2% at 360px, a common Android width. Another 13.3% had content poking past the edge that was hidden only by overflow-x: hidden on <html> or <body>.

This guide gives you the snippet, a multi-width script, the eight causes we found with before/after CSS, and why overflow-x: hidden is a band-aid.


Why a Page Scrolls Sideways

The browser makes the page scrollable horizontally when the scrollable overflow of the root element is wider than the viewport. Any element that extends past the right edge contributes, unless an ancestor clips it with overflow: hidden, clip or auto. Elements moved with transform contribute too: a menu translated 100% off-screen to the right still widens the page.

On a phone this shows up as a page that wobbles when you swipe vertically, a gap down the right side, or a layout that zooms out on load. It also fails WCAG 2.2 Success Criterion 1.4.10 Reflow, which requires content to work at 320 CSS pixels wide without two-dimensional scrolling.


What We Found on 188 Mobile Homepages

The sample was the same 219 random homepages used in our website accessibility statistics (Tranco 94GG2, ranks 1,001–50,000), of which 188 loaded in mobile emulation. Each was loaded in Chromium with BugViso's iPhone 16 Pro profile (390×844, touch, mobile user agent) and then resized to 360px.

MeasurementHomepagesShare
Scrolls horizontally at 390px115.9%
Scrolls horizontally at 360px2312.2%
Use overflow-x: hidden/clip on <html> or <body>5026.6%
…with content still extending past the edge (hidden overflow)2513.3%
Median overflow when a page scrolls16px

Most overflow is a sliver. The median scrolling page was only 16px too wide, which is just enough to make the page feel loose under your thumb. A few were dramatic: one page was 432px too wide, more than a whole extra screen.

The cause of the outermost offending element, among the 23 pages that scrolled at 360px:

CausePages
Absolutely or fixed-positioned element past the edge15
Negative margin (often a "full-bleed" hack)8
Fixed width wider than the screen6
Flex or grid child that won't shrink6
Transformed element (off-canvas menu, animation start state)5
Media without max-width, min-width too large2 each
Long unbreakable word or URL1

A page can have more than one cause, so the counts add up to more than 23. Positioned elements dominate: decorative shapes, badges, dropdowns and slide-out panels placed with right: -40px or left: 100%.


Step 1: Find the Culprit in DevTools

Open DevTools, toggle the device toolbar (Ctrl/Cmd + Shift + M), pick a 360px-wide device, and paste this into the Console:

javascript
// Lists the OUTERMOST elements crossing the viewport edge, skipping ones an ancestor clips.
(() => {
  const vw = document.documentElement.clientWidth;
  const clipped = el => { for (let p = el.parentElement; p && p !== document.body; p = p.parentElement)
    if (getComputedStyle(p).overflowX !== 'visible') return true; return false; };
  const rows = [];
  for (const el of document.querySelectorAll('body *')) {
    const r = el.getBoundingClientRect();
    if (!r.width || (r.right <= vw + 1 && r.left >= -1)) continue;
    const pr = el.parentElement !== document.body && el.parentElement.getBoundingClientRect();
    if (pr && (pr.right > vw + 1 || pr.left < -1)) continue;   // parent already overflows
    if (clipped(el)) continue;
    el.style.outline = '2px solid red';
    rows.push({ element: el, overflowPx: Math.round(Math.max(r.right - vw, -r.left)) });
  }
  console.log(`viewport ${vw}px, document ${document.documentElement.scrollWidth}px`);
  console.table(rows);
})();

Every culprit gets a red outline, and the table links to the live element (click it to jump to the Elements panel). If the table is empty but the page still scrolls, check for a pseudo-element (::before/::after), which querySelectorAll can't see. Toggle suspicious rules in the Styles pane instead.

If you suspect a parent has overflow-x: hidden hiding the problem, run this first, then the snippet again:

javascript
document.documentElement.style.overflowX = document.body.style.overflowX = 'visible';

Step 2: Fix the Cause (8 Patterns)

1. Positioned element hanging off the edge

css
/* ❌ Decorative badge pokes 40px past the right edge */
.hero-badge { position: absolute; right: -40px; }

/* ✅ Keep it inside, or clip it at the component, not the page */
.hero { position: relative; overflow: clip; }      /* clip doesn't create a scroll container */
.hero-badge { position: absolute; right: 0; }

overflow: clip (unlike hidden) doesn't create a scroll container, so position: sticky inside it keeps working. Use it at the component level, where the overflow is intentional.

2. Negative-margin "full-bleed" sections

css
/* ❌ Classic full-bleed hack: overshoots when the parent has padding or a scrollbar */
.full-bleed { margin-left: -50vw; margin-right: -50vw; left: 50%; position: relative; width: 100vw; }

/* ✅ Grid-based full bleed: no negative margins, no vw maths */
.page { display: grid; grid-template-columns: 1fr min(65ch, 100% - 2rem) 1fr; }
.page > * { grid-column: 2; }
.page > .full-bleed { grid-column: 1 / -1; }

3. Fixed widths

css
/* ❌ */ .promo { width: 420px; }
/* ✅ */ .promo { width: 100%; max-width: 420px; }

Search your CSS for width: and min-width: values above 320px that aren't inside a media query. Each one is a candidate.

4. Flex and grid children that won't shrink

Flex items default to min-width: auto, so they won't shrink below their content's minimum size. A long word, a wide image or a nested flex row inside them pushes the row wider than the screen.

css
/* ✅ Allow flex/grid children to shrink below their content size */
.card-row > * { min-width: 0; }
.layout { grid-template-columns: minmax(0, 1fr) minmax(0, 2fr); }   /* not 1fr 2fr */

5. Off-canvas menus and transforms

css
/* ❌ Hidden menu sits translated off-screen and still widens the page */
.drawer { position: absolute; top: 0; right: 0; width: 80vw; transform: translateX(100%); }

/* ✅ Fixed positioning takes it out of the page's scrollable area; hide it from AT too */
.drawer { position: fixed; inset: 0 0 0 auto; width: 80vw; transform: translateX(100%); visibility: hidden; }
.drawer.is-open { transform: none; visibility: visible; }

6. Long words, URLs and email addresses

css
/* ✅ Break long tokens only when they'd overflow */
.content { overflow-wrap: anywhere; }      /* or break-word for older engines */
.content a { word-break: break-word; }

One page in our sample scrolled 75px sideways at 390px because of long URLs printed as link text. overflow-wrap fixes this without hyphenating normal words.

7. Images, video and embeds

css
/* ✅ Media never exceeds its container */
img, video, iframe, canvas, svg { max-width: 100%; height: auto; }

For third-party <iframe> embeds with fixed width attributes, wrap them in a container with aspect-ratio and set the iframe to width: 100%.

8. Tables and code blocks

css
/* ✅ Scroll the component, not the page */
.table-wrap, pre { overflow-x: auto; max-width: 100%; }

Wrap wide tables in a <div class="table-wrap" tabindex="0" role="region" aria-label="Pricing table"> so keyboard users can scroll it too. WCAG's reflow criterion explicitly allows two-dimensional scrolling for data tables and code.


Why overflow-x: hidden on body Is a Band-Aid

Adding body { overflow-x: hidden; } makes the scrollbar go away. 26.6% of the homepages in our sample did exactly that, and on half of those, content was still sticking out past the edge, just invisible. The problems with this "fix":

  • Content gets cut off. Whatever overflowed is now clipped: the end of a button, a menu item or a form field.
  • It breaks position: sticky. overflow: hidden on an ancestor makes it the scroll container, so sticky headers inside stop sticking.
  • Mobile browsers don't always honour it on <body>. In some mobile browsers, overflow-x: hidden on body alone doesn't stop sideways panning. Teams then add it to html too and lose sticky positioning everywhere.
  • It hides future regressions. The next overflow nobody notices is also clipped, so the page never visibly breaks until a user can't reach a button.

If you need page-level clipping as a stopgap, overflow-x: clip on the specific section is safer than hidden on the body. Treat it as temporary and fix the element.


Check Every Width With One Script

DevTools tests one width at a time. This Playwright script checks several widths per URL, lists the outermost offenders with a probable cause, and then re-checks with overflow-x forced visible to reveal hidden overflow:

python
#!/usr/bin/env python3
"""find_overflow.py: find the elements that make a page scroll sideways on phones.

Usage:
    pip install playwright && playwright install chromium
    python3 find_overflow.py https://example.com [https://example.com/pricing ...] [--widths 320 360 390 412]
"""
import argparse, asyncio

from playwright.async_api import async_playwright

PROBE = r"""() => {
  const vw = document.documentElement.clientWidth;
  const clipped = (el) => { for (let p = el.parentElement; p && p !== document.body && p !== document.documentElement; p = p.parentElement)
      if (getComputedStyle(p).overflowX !== 'visible') return true; return false; };
  const label = (el) => el.tagName.toLowerCase() + (el.id ? '#' + el.id : '') +
      (typeof el.className === 'string' && el.className.trim() ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.') : '');
  const why = (el, r) => { const st = getComputedStyle(el), p = el.parentElement;
    if (/^(IMG|VIDEO|IFRAME|CANVAS|svg|EMBED|OBJECT)$/.test(el.tagName)) return 'media without max-width';
    if (el.tagName === 'TABLE') return 'wide table';
    if (el.tagName === 'PRE' || el.closest('pre')) return 'unwrapped code block';
    if (st.transform !== 'none') return 'transform (off-canvas / animation start state)';
    if (st.position === 'absolute' || st.position === 'fixed') return 'positioned element';
    if (parseFloat(st.marginLeft) < 0 || parseFloat(st.marginRight) < 0) return 'negative margin';
    if (parseFloat(st.minWidth) > vw) return 'min-width wider than screen';
    if (p && /flex|grid/.test(getComputedStyle(p).display)) return 'flex/grid child that will not shrink';
    if ((el.textContent || '').split(/\s+/).some(w => w.length > 25) && st.overflowWrap === 'normal') return 'long unbreakable word/URL';
    return 'fixed width'; };
  const out = [];
  for (const el of document.querySelectorAll('body *')) {
    const r = el.getBoundingClientRect();
    if (r.width === 0 || r.height === 0 || (r.right <= vw + 1 && r.left >= -1)) continue;
    const st = getComputedStyle(el); if (st.visibility === 'hidden' || st.display === 'none') continue;
    const pr = el.parentElement && el.parentElement !== document.body ? el.parentElement.getBoundingClientRect() : null;
    if (pr && (pr.right > vw + 1 || pr.left < -1)) continue;          // report the outermost offender only
    if (clipped(el)) continue;
    out.push({el: label(el), px: Math.round(Math.max(r.right - vw, -r.left)), why: why(el, r)});
    if (out.length >= 8) break;
  }
  return {vw, scrollW: document.documentElement.scrollWidth, items: out};
}"""
UNMASK = "() => { document.documentElement.style.setProperty('overflow-x','visible','important'); document.body.style.setProperty('overflow-x','visible','important'); }"


async def check(browser, url, width):
    ctx = await browser.new_context(viewport={"width": width, "height": 800}, device_scale_factor=2,
                                    is_mobile=True, has_touch=True)
    page = await ctx.new_page()
    await page.goto(url, wait_until="load", timeout=45000)
    await page.wait_for_timeout(1500)
    before = await page.evaluate(PROBE)
    await page.evaluate(UNMASK)
    await page.wait_for_timeout(200)
    after = await page.evaluate(PROBE)
    await ctx.close()
    return before, after


async def main(urls, widths):
    async with async_playwright() as pw:
        browser = await pw.chromium.launch()
        for url in urls:
            print(f"\n{url}")
            for w in widths:
                before, after = await check(browser, url, w)
                scrolls = before["scrollW"] > before["vw"] + 1
                hidden = not scrolls and after["scrollW"] > after["vw"] + 1
                state = (f"✗ scrolls sideways by {before['scrollW'] - before['vw']}px" if scrolls else
                         f"⚠ hidden overflow: {after['scrollW'] - after['vw']}px clipped by overflow-x" if hidden else "✓ fits")
                print(f"  {w}px  {state}")
                for i in (before if scrolls else after)["items"] if (scrolls or hidden) else []:
                    print(f"        +{i['px']:>4}px  {i['el'][:60]:60s} {i['why']}")
        await browser.close()


if __name__ == "__main__":
    ap = argparse.ArgumentParser()
    ap.add_argument("urls", nargs="+")
    ap.add_argument("--widths", nargs="+", type=int, default=[320, 360, 390, 412])
    a = ap.parse_args()
    asyncio.run(main(a.urls, a.widths))

Real output for two homepages from our sample (9 October 2026, names replaced):

text
https://ip-tools.example/
  360px  ✗ scrolls sideways by 461px
        + 390px  div#ad_container                                             negative margin
        + 392px  div#container.clearfix                                       negative margin
  390px  ✗ scrolls sideways by 432px
        + 360px  div#ad_container                                             negative margin
        + 362px  div#container.clearfix                                       negative margin

https://furniture-shop.example/
  360px  ⚠ hidden overflow: 20px clipped by overflow-x
        + 380px  nav#js-product-nav.product-nav.has-motd                      positioned element
        +   5px  a.back-to-top.js-back-to-top                                 positioned element
        +  20px  button.form-control.btn                                      positioned element
  390px  ⚠ hidden overflow: 15px clipped by overflow-x
        + 405px  nav#js-product-nav.product-nav.has-motd                      positioned element

The second site looks fine to visitors, but a positioned navigation panel and a button cross the edge, and overflow-x is hiding them. That last button is probably cut off for real users.


How BugViso Detects Horizontal Overflow

BugViso's mobile pass loads every scanned page in Chromium emulating an iPhone 16 Pro (390×844, DPR 3, touch, mobile user agent) and runs a mobile-friendliness check. It flags horizontal overflow when the document's scroll width exceeds the viewport, and reports the element whose right edge extends furthest past it, with its selector and how many pixels it overshoots. The same pass checks the viewport meta tag, tap-target sizes against WCAG 2.5.8, and touch-target collisions, and records mobile Core Web Vitals.

One honest caveat: the reported element is the furthest-right one, which is sometimes a slide inside a clipped carousel rather than the element actually causing the scroll. When that happens, run the DevTools snippet above on the same page, because it walks up to the outermost unclipped offender. BugViso also checks a single width (390px). Since overflow doubled between 390px and 360px in our data, test narrower widths with the script.

You can run a free mobile check with BugViso, and work through the mobile SEO audit checklist for everything else on small screens. Everything in the mobile pass is listed on the WCAG accessibility and mobile checks page.


Traps and Edge Cases

  • 100vw includes the scrollbar on desktop. width: 100vw is wider than the page whenever a classic scrollbar is showing, which adds sideways scroll on Windows desktops. Phones use overlay scrollbars, so the bug only shows on desktop. Use width: 100%.
  • Animations start off-screen. Elements animated in from translateX(100%) widen the page until the animation runs, which may be never if it's triggered by scrolling into view.
  • vw units in fonts. font-size: 6vw headings can produce words wider than the container on narrow screens. Clamp them: font-size: clamp(1.75rem, 6vw, 3.5rem).
  • Zoomed-out pages. If the viewport meta tag is missing, mobile browsers render at a 980px layout width and zoom out. That's not overflow, but it looks like it. Check for <meta name="viewport" content="width=device-width, initial-scale=1">.
  • Test at 320px. WCAG's reflow test uses 320 CSS pixels (the width of a 1280px window at 400% zoom), and it catches problems that 360px misses.

FAQ

How do I find which element is causing horizontal scroll?

Run the DevTools snippet in this guide on a narrow device preset. It lists and outlines the outermost elements that cross the viewport edge, skipping anything already clipped by a parent.

Should I use overflow-x: hidden on the body?

Only as a temporary stopgap. It hides the symptom, clips content, breaks position: sticky and doesn't work reliably on every mobile browser. Fix the element that's too wide.

Why does my site scroll sideways on Android but not on iPhone?

Common Android widths (360px, 384px, 412px) differ from iPhone widths (375px–440px). In our data, twice as many homepages overflowed at 360px as at 390px. Always test the narrowest width your audience uses.

Does horizontal scroll affect SEO?

Google uses mobile-first indexing, so it evaluates your mobile layout, and content that's hard to reach on a phone hurts usability. It's also a WCAG 2.2 reflow failure, which matters for accessibility compliance.


Conclusion

Horizontal scroll is almost always one element that's too wide: find the outermost offender, fix its width, margin or position, and stop hiding it with overflow-x: hidden. A BugViso mobile scan flags the overflow on every page it audits.

Found this useful? Share it.

See where your site stands

Run a free BugViso audit for SEO, speed, accessibility and AI search readiness — with fixes you can ship today.