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
andoverflow-y
) that can be set toauto
orscroll
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”