A page scrolls sideways when one element is wider than the viewport. The usual causes: fixed pixel widths, 100vw inside padding, long unbroken strings, wide tables and code blocks, images and iframes with width attributes, flex or grid children that cannot shrink, and elements positioned or transformed past the right edge. Find the element first; overflow-x: hidden only hides it.
Key takeaways
- Every case in the tests was one element wider than the viewport. There is no mystery source; the job is finding which element.
- A short console snippet names the offender. It lists the elements whose right edge is past the viewport, widest first.
- Each cause has a one-line fix, and every fix brought the page back to exactly the viewport width at 390px.
- Past the right edge scrolls; past the left edge does not. The same decorative box at right: -100px made the page 474px wide, at left: -100px it stayed 390px.
- width: 100vw is wider than the page on desktop too when the browser has a classic scrollbar: 1,280px against 1,265px.
- overflow-x: hidden on html and body hides the problem and breaks sticky positioning. It also cuts off the content that was too wide.
Find the element first
Open DevTools, switch to a phone width (390px is a common one) and paste this into the console. It lists elements whose right edge is past the viewport, widest first:
const w = document.documentElement.clientWidth;
[...document.querySelectorAll('body *')]
.filter(el => el.getBoundingClientRect().right > w + 0.5)
.map(el => ({ el, right: Math.round(el.getBoundingClientRect().right) }))
.sort((a, b) => b.right - a.right)
.slice(0, 5)
.forEach(x => console.log(x.right, x.el));
On a test page with a flex row holding a long URL, it printed one line: the
.cell element, at 893px. Elements inside a deliberate scroller, such as a table
in an overflow-x: auto wrapper, will also be listed even though they do not
widen the page, so check the top result first.
The 8 causes, measured at 390px
Each row is a small test page with one offender, in a mobile viewport 390px wide. "Page width" is the document's scroll width: 390 means no sideways scroll.
| Cause | Page width | Fix | After |
|---|---|---|---|
1. Fixed pixel width (width: 500px) | 516 | max-width: 100% | 390 |
2. width: 100vw inside a padded container | 406 | width: 100% | 390 |
2b. width: 100vw on desktop with a classic scrollbar | 1,280 vs 1,265 | width: 100% | 1,265 |
| 3. Long unbroken string, such as a URL | 893 | overflow-wrap: anywhere | 390 |
| 4. Wide table (six no-wrap columns) | 959 | Table inside an overflow-x: auto wrapper | 390 |
5. A long line inside pre | 1,943 | pre { overflow-x: auto } | 390 |
6. Image with width="800" | 816 | img { max-width: 100%; height: auto } | 390 |
6b. iframe with width="560" | 580 | iframe { max-width: 100% } | 390 |
| 7. Flex child holding a long string | 893 | min-width: 0 plus overflow-wrap on the child | 390 |
7b. Grid with a 1fr column and a long string | 893 | grid-template-columns: minmax(0, 1fr) | 390 |
8. Absolutely positioned box at right: -100px | 474 | Keep it inside, or wrap it in overflow: clip | 390 |
8b. transform: translateX(100%) | 732 | Wrap it in overflow: clip | 390 |
Same box at left: -100px | 390 | No fix needed | - |
Three of these are worth a second look. Flex and grid items default to a minimum size
based on their content, so one long word can stretch the whole layout; which is the reason
to declare a grid column minmax(0, 1fr) rather than 1fr, as this
site's article layout does. Images are the same problem with a different trigger: give them a
max-width as above, and dimensions too, as the
layout-shift guide explains. And
100vw includes the scrollbar, which is why it overflows on desktop even
when nothing looks wrong.
Why not just hide it
overflow-x: hidden on html and body does make the
scrollbar disappear: both that and clip brought a 500px box on a 390px page
back to a 390px page width. It has two costs. The content past the edge is still cut off,
so a table or a button can be unreachable. And on both elements together it breaks
position: sticky for everything on the page, as the
sticky guide measures.
overflow: clip instead of setting overflow on the whole page.
That is the pattern behind the last two rows of the table, and it leaves sticky alone.
Testing it properly
The check is one comparison: is the document's scroll width greater than its client width? Run it at 390px on every template, not just the home page. This site's audit does exactly that for all of its pages, and the same check is also on the pre-launch checklist as a "test on an actual phone" item.
const sideways = document.documentElement.scrollWidth
> document.documentElement.clientWidth;
console.log(sideways ? 'scrolls sideways' : 'fits');