Resizable HTML Scroll Boxes

CSS offers two ways to shrink wide elements so that they fit a narrower browser window:

  • The white-space property whose default value of normal breaks long text lines at spaces, as in a word processor.
  • The overflow property (also available as overflow-x and overflow-y) that can be set to auto or scroll to put wide content into a scroll box.

What CSS doesn’t offer is a resizable scroll box. An overflow scroll box always has a fixed size that’s determined when the page is first rendered. For a horizontal scroll box, that size then serves as the minimum width for all paragraphs in the same flow, preventing any white-space line breaks below that width.

But we want both text paragraphs and scroll boxes to respond dynamically to changing browser windows – text by wrapping and boxes by resizing. That’s not possible in pure CSS so we need some JavaScript.

Element Markup

First, we need to put our prospective scroll boxes around any elements that need scrolling. In my case, I wanted to make several wide tables scrollable and resizable, so I wrapped them in div elements annotated with a scroll class that’s not used for anything else:

<div class="scroll"><table>
  <em>…very wide table follows…</em>
</table></div>

The table itself remains unchanged. In my CSS style sheet, div.scroll gets the same minimum width as the surrounding text paragraphs but no other properties. That makes the new div element completely inert when the user has disabled JavaScript – the page will render just as if we hadn’t changed anything.

Viewport Width

Next, we need to figure out the current width of the browser’s viewport. There is some confusion here. Many websites and even David Flanagan’s JavaScript: The Definitive Guide (O’Reilly 2011, 6th ed., p. 391f) erroneously claim that the JavaScript properties window.innerWidth and document.documentElement.clientWidth (or in Quirks mode, document.body.clientWidth) are interchangeable.

But as a matter of fact, the innerWidth property includes the width of the vertical browser scroll bar, if present, while the clientWidth properties exclude it. Since we’re interested in the viewport area that’s actually visible we must use clientWidth. A Tale of Two Viewports offers a detailed discussion of viewport behavior on both desktop and mobile platforms. For our purposes, this is the function we need:

function getViewportWidth() {
  return (document.compatMode == "CSS1Compat" ?
    document.documentElement.clientWidth :
    document.body.clientWidth);
}

Automatic Resizing

Finally, the core of the algorithm. We attach an event handler to window.onresize (and onload, see below) that grabs all div.scroll elements on the current page, checks the widths of their first children (i.e. our wide tables) against the currently available viewport space, and explicitly resizes the div containers if necessary.

window.onload = enable_scrolling;
window.onresize = enable_scrolling;

function enableScrolling() {
  var scroll_divs = document.querySelectorAll("div.scroll");
  if (scroll_divs.length == 0) return;

  var viewportWidth = Number(getViewportWidth());

  for (var i = 0; i < scroll_divs.length; i++) {
    var scroll_div = scroll_divs[i];
    var scroll_divLeft = Number(scroll_div.getBoundingClientRect().left);

    // scroll range is governed by width of first child
    var scroll_width = Number(scroll_div.firstChild.scrollWidth);

    // leave equal margin on both sides of scroll division
    var scroll_divWidth = viewportWidth - 2 * scroll_divLeft;
    if (scroll_divWidth < 0) scroll_divWidth = 0;

    if (scroll_width > scroll_divWidth) {
      // width is bounded by minimum width on class style
      scroll_div.style.width = scroll_divWidth + "px";
      scroll_div.style.overflowX = "scroll";
    } else {
      scroll_div.style.width = null;
      scroll_div.style.overflowX = "visible";
    }
  }
}

I’ve tested this function on the current versions of Chrome, Firefox, and Internet Explorer on Windows, as well as Mobile Safari on iOS and Chrome on Android. It should work even on older Internet Explorer versions back to IE7, although I haven’t tested that. Some notes on the implementation:

  • I don’t like content touching the window border, so I subtract the left margin twice from the actual viewport width, creating the same margin on the right side. (updated 2017-12-24)
  • Rather than explicitly checking against a minimum width, I’m relying on the minimum width that was specified for div.scroll in the associated CSS style sheet.
  • Alternatively, simply disable line wrapping in tables using white-space: nowrap; so that each table defines its own minimum width. (added 2014-02-22)
  • I’m casting obsessively to Number because, in an earlier version, the foolish JavaScript type system mistook a numerical expression for a string comparison…

To see this script in action, visit e.g. the RNG Range Projection page which has a nice big table. Scroll down to the table and narrow your browser window. The table will shrink along with the surrounding text and show a scroll bar. Of course you could also put any other fixed-size content in these auto-resizing scroll boxes, such as images.

Mobile Browsers

As noted above the script does work on mobile browsers, and will automatically put wide tables into a scroll box instead of shrinking the entire page to fit the mobile viewport. That’s great. What’s not so great is that no scroll bar appears.

The scrolling functionality is intact, you can swipe the table right and left to scroll it, but there’s no visible indication of this fact – the table simply looks cut off. Lack of visual feedback is a notorious problem on touchscreen platforms. I’m not sure what to do about this, other than wait for OS makers to get a clue and stop hiding functionality.

2013-04-08: While experimenting with mobile themes, I discovered that the event handler must be attached to window.onload as well as onresize. Otherwise a mobile browser might automatically enlarge its virtual viewport before we get a chance to enable scrolling.

One thought on “Resizable HTML Scroll Boxes”

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.