Let’s suppose you want to generate a layout whereby there is a header and then any number of independent scrollable areas beneath. The header should remain in place while any of the panels below are scrolled. Furthermore, the panels below that scroll should NOT instigate the body scrolling.

This was the situation I found myself in and at present, I’m fairly happy with the solution I arrived at (which is seldom the case).

Ingredients and fussy diners

The solution makes use of the new(ish) CSS vh units which at this point are moderately well supported (notable absentees are Android 4.3 and below – sad face) and Flexbox (will work most places other than <IE9). Are you kidding me Frain? No stinking Android 2–4.3 support?

My use case was a fully responsive design and the layout for smaller viewports (the majority of Android devices in my scenario) didn’t require this treatment. I only adjusted to this view for very large screens (probably beyond the majority of tablets). So, yes, no Android 2–4.3 support (unless you introduce JavaScript which invalidates the challenge). Deal with it, OK?

The example

If you want to open the test page (fair warning, it’s not pretty), you can view it here: http://benfrain.com/playground/scroll-test.html

The markup

OK, let’s do this. First the markup:

<!DOCTYPE html>
<html class="no-js">
    <head>
        <meta charset="utf-8">
        <title>Independent CSS scrolling panels (with inertia)</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="css/main.css">
    </head>
    <body>
    <div class="Top">Top Content</div>
    <div class="Container">
        <div class="Left">Left Content</div>
        <div class="Middle">Middle Content</div>
        <div class="Right">Right Content</div>
    </div>
    </body>
</html>

The CSS

Now the CSS. I’ve commented the various sections below:

/*I love me some border-box*/
* {
    box-sizing: border-box;
}
/*This just stops me getting horizontal scrolling if anything overflows the width*/
body {
    overflow-x: hidden;
}
/*Just removing default browser padding/margin*/
html,
body {
    padding: 0;
    margin: 0;
    color: #ebebeb;
}
/*Flexbox gives us the flexiness we need. The top just stays put as there is no scrolling on the body due to the page never exceeding viewport height*/
.Top {
    display: flex;
    align-items: center;
    justify-content: center;
    background-color: darkgreen;
    font-size: 3rem;
    position: relative;
    z-index: 10;
    height: 100px;
}
/*This is our main wrapping element, it's made 100vh high to ensure it is always the correct size and then moved into place and padded with negative margin and padding*/
.Container {
    display: flex;
    overflow: hidden;
    height: 100vh;
    margin-top: -100px;
    padding-top: 100px;
    position: relative;
    width: 100%;
    backface-visibility: hidden;
    will-change: overflow;
}
/*All the scrollable sections should overflow and be whatever height they need to be. As they are flex-items (due to being inside a flex container) they could be made to stretch full height at all times if needed.
WebKit inertia scrolling is being added here for any present/future devices that are able to make use of it.
*/
.Left,
.Middle,
.Right {
    overflow: auto;
    height: auto;
    padding: .5rem;
    -webkit-overflow-scrolling: touch;
    -ms-overflow-style: none;
}
/*Entirely optional – just wanted to remove the scrollbar on WebKit browsers as I find them ugly*/
.Left::-webkit-scrollbar,
.Middle::-webkit-scrollbar,
.Right::-webkit-scrollbar {
    display: none;
}
/*  Left and Right are set sizes while the Middle is set to flex one so it occupies all remaining space. This could be set as a width too if prefereable, perhaps using calc.*/
.Left {
    width: 12.5rem;
    background-color: indigo;
}

.Middle {
    flex: 1;
}

.Right {
    width: 12.5rem;
    background-color: violet;
}

Where does this work?

All the evergreen browsers render as expected. You’ll need to ensure you are Autoprefixing the Flex properties (there are no flex prefixes included in the above example for the sake of brevity but they are in the linked example above) if you want Safari to play ball. Once that is done, Safari works as expected too.

Notes

Positioning with calc if preferable is support allows

To position the main content in the above code I have used negative margin to pull it into place and padding at the top so content starts as expected. This decision was purely for better browser support. A cleaner approach would be calc:

.Container {
    display: flex;
    overflow: hidden;
    height: calc(100vh - 100px);
    position: relative;
    width: 100%;
}

Performance concerns

I’ve written this technique up for purely selfish purposes. I’m using this technique on a project and happened to read Paul Lewis’s post, Some Gotchas That Got Me in which he seems to describe a similar technique and notes that:

The container element doesn’t get compositor-supported fast scrolling in all browsers. That means slow scrolling and paints on every scroll change. Which generally means you just killed performance.

I’d already tested this technique with continuous paint mode and not encountered any issues (obviously I’ve used a loaded DOM, not this simplistic example) but I wanted Paul’s further input on where the limits are to this, performance wise. He was kind enough to respond and I’ll quote the relevant details of the response below.

OK, let’s see if I can provide the it’s-bonkers-but-that-is-what-it-is version:

Chrome has to deal differently with overflow: scroll elements compared to the body.

Historically these elements didn’t get compositor promotion so essentially when the content changed it would get repainted. Obviously overflow: scroll is a strong hint that we want scrolling so we want the compositor to help.

Chrome also behaves differently on hi vs low DPI. Elements get promoted in Hi DPI contexts, so mobile & retina MacBook Pros are A+ as of about Chrome 40. Generally we are careful with promotion because it affects text rendering, and this is problematic on low DPI where you would notice a switch from subpixel antialiasing to grayscale.

You can still fall off the Hi DPI fast path if the element either has a clip specified OR a border-radius.

If you are on a low DPI screen things are less good; you don’t get automatic promotion. In theory you can promote the element and its immediate children with -webkit-backface-visibility: hidden and, while you may see a paint, it should be essentially an empty white block i.e. virtually free. I think the same caveat as above applies.

I’ve said nothing here about other browsers, because I simply don’t have the data. For what is is worth, Chrome engineers are working super hard to make all of this sane!

Summary

My hope was for a consistent way to provide independently scrollable panels without always reaching for iScroll et al.
Despite the caveats regarding performance (and the very real need for me to test on different UAs), where support allows, this provides a pretty consistent and lightweight solution.