How to make horizontal scrolling work well in Android browsers

I recently created a small web page that scrolls horizontally instead of vertically. Making it work well in web browsers on Android required a custom solution, which I’ll explain in this blog post.

But first, this is how the page looks:

Android smartphone shows web page in web browser. The web pages extends do the right of the smartphone.
Try it out at switch2.šime.eu

While creating this page, I wanted to achieve the following behavior:

  1. The content should be full height (as tall as the viewport).
  2. The page should scroll horizontally.
  3. The page should not scroll vertically.

Additionally, all three requirements should continue to be met when the user rotates their phone sideways and Android switches to landscape mode. In other words, I want the same user experience regardless of whether the user holds their phone upright or sideways.

Seems simple enough. Let’s try to make it happen, step by step. (Note: This blog post is longer than it needs to be for educational purposes. If you just want my suggested solution, skip to step 4.)

Step 1: Have the page be very wide

To test how Android browsers handle pages that are very wide, we’ll use a <div> with the following dimensions:

#content {
  width: 3000px;
  height: 95svh;
}
Why 95svh instead of 100svh?

In desktop browsers with classic scrollbars, the horizontal scrollbar at the bottom of the viewport takes up some vertical space (around 17px), so an element that is 100svh tall no longer fits into the viewport, which causes an undesired vertical scrollbar on the viewport — it’s an entire different problem. I wrote a detailed article about that topic. For my simple page, 95svh will be just fine.

Of course, the page needs to have the viewport meta tag:

<meta name="viewport" content="width=device-width">

Test page 1

In Android browsers, the test page is zoomed out on page load. The content is not full height. On my phone, the initial zoom level is around 25%.

  1. full height ❌
  2. scrolls horizontally ✅
  3. doesn’t scroll vertically ✅

It looks like Android browsers detect that the page is too wide and reduce the zoom level to show more of the page content on page load. This is a good default behavior, I guess, but our page is designed to be full height and horizontally scrollable, so we don’t want this behavior. Luckily, it can be disabled.

Step 2: Force the initial zoom level to be 100%

Adding initial-scale=1 to the viewport meta tag tells browsers not to zoom out the page on page load, regardless of how wide it is.

Test page 2

Android browsers show this test page at the default zoom level (100%). The content is full height. But now there’s an additional vertical scrollbar. If the user scrolls down, the content scrolls completely out of view. This is not only useless (there is nothing below the content), but it’s a bad user experience because the user may accidentally swipe up while trying to swipe left or right, or they may instinctively start scrolling down after the page loads and be confused about all the empty space below the content.

  1. full height ✅
  2. scrolls horizontally ✅
  3. doesn’t scroll vertically ❌
Why do Android browsers add this vertical scrollbar? I have a theory.

The user can zoom out (via pinch gestures) until the entire page, in this case the 3000px-wide <div>, is visible in the viewport. The <div> is rendered at the top of the viewport, and there is white space beneath it. This viewport is the layout viewport. The width of this viewport is 3000px (based on the page content), and its aspect ratio is the same as the aspect ratio of the visual viewport. Let’s say this aspect ratio is 3:5. Then the height of the layout viewport is 5000px. When the browser first loads the page, it determines the dimensions of the layout viewport based on the page content and the aspect ratio of the visual viewport: the layout viewport becomes 3000px wide and 5000px tall. Then the browser zooms in to 100% because the page sets initial-scale=1. Now the visual viewport, whose dimensions are around 300px by 500px, is smaller than the layout viewport (whose dimensions don’t change), and that’s why the browser provides both horizontal and vertical scrollbars: to allow the user to freely move the visual viewport within the layout viewport in both directions. This still doesn’t explain why there is no vertical scrollbar on test page 1 (at least not initially). My guess is that when the browser can freely set the initial zoom level to whatever it thinks is the optimal value (test page 1), it can decide to not show the vertical scrollbar if it thinks that it’s not necessary, but when the page forcefully sets the initial zoom level via initial-scale (test page 2), the browser thinks, “The page might be doing something weird with the zoom. I better provide all the scrollbars for my user, just in case.”

It’s difficult to make a web page work the way you want if you don’t understand why browsers behave the way they do. The vertical scrollbar seems to be related to the zoom level. When the page is fully zoomed out, the vertical scrollbar disappears. The vertical scrollbar is present so that the user can zoom out. Is that it? So then maybe if we disable zooming out, the vertical scrollbar will go away.

Step 3: Disable zooming out below 100%

In addition to initial-scale, there also exists minimum-scale. If both are set to the same value (1), the initial zoom level (100%) also becomes the minimum zoom level. The user can zoom in to 200% and zoom back out to 100%, but they can’t zoom out to 50%.

Test page 3

Android browsers indeed stop showing a vertical scrollbar by default. However, if the user force-enables zoom in the browser’s settings (“Zoom on all web sites” in Firefox, “Force enable zoom” in Chrome), the vertical scrollbar reappears.

The mentioned accessibility option exists because some websites didn’t want to allow users to zoom in (above 100%). This option doesn’t really have anything to do with zooming out below 100%. The whole concept of zooming out below 100% doesn’t really exist in Android browsers in practice. Normal websites that scroll vertically cannot be zoomed out below 100%. It’s unfortunate that an accessibility option that exists for an unrelated reason gets in the way of implementing a web page that scrolls horizontally.

  1. full height ✅
  2. scrolls horizontally ✅
  3. doesn’t scroll vertically ✅ (default) ❌ (when zoom is force-enabled)

I don’t know how to reliably achieve the desired behavior with the default viewport scroller. Maybe there is a way, but I haven’t found it (so far). Luckily, the viewport scroller isn’t strictly necessary. We can let an element on the page do the scrolling instead.

Step 4: Put the content in a custom scroll container

We wrap the page content in a <div> and let that <div> act as our horizontal scroll container. By containing the scroll functionality within an element that itself does not overflow the viewport, we completely avoid the issue of the enlarged layout viewport and the resulting vertical scrollbar on the viewport scroller.

#scroller {
  height: 100svh;
  overflow-x: auto;
}
100svh? Didn’t you say 95svh?

In this case, the #scroller element is the scroll container that scrolls the content. The horizontal scrollbar belongs to this element and is contained within it. This element needs to have the full viewport height (100svh) so that the scrollbar appears at the very bottom edge of the viewport, as if it was the scrollbar of the viewport itself. The #content element inside the #scroller element still has a height of 95svh, for the same reason as before.

Test page 4

As far as I can tell, this approach works flawlessly. The user can zoom in normally. When the phone is rotated sideways, the content remains full height. We don’t even need the initial-scale=1 and minimum-scale=1 properties in the viewport meta tag anymore.

  1. full height ✅
  2. scrolls horizontally ✅
  3. doesn’t scroll vertically ✅

Reply on Mastodon