Everyday3d
Posted on 2014-08-18

Smooth scrolling with VirtualScroll

Black shape of light

Parallax scrolling websites may not be the cool new thing anymore, but having the content react to the scroll in some fancy way has become a popular interaction pattern.

When used in moderation, it can enhance the user experience and take the interface of a website to the next level. There are many ways to achieve scrolling effects, but the trick is always to make it as smooth as possible. Below I describe a technique I use to achieve scrolling effects on my projects.

Hi! This is Bartek from March 2017. I came back in time to tell you, that using Virtual Scroll can be great for certain things, like WebGL or Canvas interactive pieces. But it is never a good idea to use it on a website as a replacement of the default browser scroll. Trust me, I learned this the hard way. With this in mind, please continue reading :)

When I started playing with different parallax-type effects, I quickly realized that the browser built-in scroll function, window.onscroll, is not very helpful. On a mobile device this event is fired only once after the scrolling action stops, so no animation is possible. On desktop it works a bit better, but there are other limitations. My biggest concern tough, was that I couldne't get the fine level of control over the animation and easing I wanted.

I figured that a good approach would be to disable the browser scroll entirely and move the content using CSS transforms based on values read from an event listener. On desktop, I would listen to the mouse wheel event; on a touch screen, to the touch events. I named this pattern the VirtualScroll.

The first step is to disable the browser built-in scroll. To achieve this, just set your CSS on the body element to:

body { overflow:hidden; }

This will lock the content and prevent browser from scrolling. On touch screen devices, you need to do one more thing: prevent the touchmove event from doing its job of scrolling the site. This is how ite's done:

document.addEventListener('touchmove', function(e) { e.preventDefault(); });

Next, you need to create a section of the HTML document that will be animated using VirtualScroll. Of course there are tons of ways of doing it, starting from a simple declarative approach and ending with apps that use AJAX to load and populate the DOM. Whichever way you do it, you should end up with a div or section tag holding all your scrollable content.

Now that the content is ready, we need to grab the height of the element holding it. To do this, there is actually a simple method that seems to work in all major browsers. Assuming that you used a section tag to host your content, it goes like this:

var section = document.querySelector('section'); var sectionHeight = section.getBoundingClientRect().height;

This technique has some issues when CSS margins are used. The element needs to be positioned absolutely in order for it to take margins into account when returning the height. Ite's one of those weird, hard to explain features of CSS I guess… Anyway, what you need to do is simply:

section { position: absolute; }

The next step is to listen to the scroll events in Javascript. This is a bit complex, because different browsers treat mouse wheel events in a different way and also because we want to support several different modes of interaction – mouse, touch, keyboard. Here is the code I use. It deals with browser differences when it comes to the mouse wheel events, has listeners for touch events and also for keyboard events, so you can scroll the content using the cursor keys. Using it is very simple and ite's 100% dependency free. Just include VirtualScroll.js in your HTML document and use it this way:

VirtualScroll.on(function(e) { // e is an object that holds scroll values, including: e.deltaY; // <- amount of pixels scrolled vertically since last call e.deltaX; // <- amount of pixels scrolled horizontally since last call }

The callback is fired whenever the user uses his touch pad, mouse wheel, taps and drags the touch screen or presses a cursor key. Note that you should not use this function to move your content. Use this function only to accumulate the delta values and clamp them so that your scrolling content doesn't go offscreen. The animation is done elsewhere (see below).

For simplicity sake, let's assume you just want to scroll your content vertically. Here's how you can track the value you need:

var targetY = 0; VirtualScroll.on(function(e) { targetY += e.deltaY; targetY = Math.max( (scrollHeight - window.innerHeight) * -1, targetY); targetY = Math.min(0, targetY); }

The clamping of the targetY values above may require some explanation. Our scrolling animation will work by moving the HTML element inside the browser window using CSS transforms. To scroll down, we need to move the element up, above the top border of the browser window. So, in other words, we need to translate it on the Y axis in the negative direction. The furthest it should move is the height of the content itself minus the height of the window. This way the scroll will stop just when the bottom of the content reaches the bottom of the screen. On the other end, we should stop it at 0, because otherwise the top of the content will scroll below the top of the window.

Clamping the value is not mandatory however. If you want to use the VirtualScroll values in a different way, you can get creative with it. For example, you can do a scrolling pattern that repeats the content infinitely or infinitely loads more content (infinite scroll). You can do try a radial scrolling where things turn in round or do whatever else you want!

The CSS translation is done inside another function, one that runs at every frame, ideally 60 times per second. The best way to make a loop like this is to use requestAnimationFrame:

var currentY = 0, ease = 0.1; var run = function() { requestAnimationFrame(run); currentY += (targetY - currentY) * ease; var t = 'translateY(' + currentY + 'px) translateZ(0)'; var s = section.style; s["transform"] = t; s["webkitTransform"] = t; s["mozTransform"] = t; s["msTransform"] = t; } run();

What happens here is that, the run function is executed on every frame thanks to the call to requestAnimationFrame at the beginning. Every time it is called, it uses a simple equation to move the value of currentY closer to the to targetY value.

The amount of easing depends on the value of the ease variable. I find values between 0.1 and 0.2 to give the best results. As the value gets closer to 1, the easing becomes less pronounced. Very small values on the other hand, like 0.001, will give a lot of easing and might make the site feel unresponsive. Experiment to see what you like best.

Notice the initial call to run() in the last line. Don't forget that, otherwise nothing will happen (I tend to make this mistake a lot... :)

The CSS transform does simply move the element along the Y axis. It also adds a 0 translation on the Z axis to make sure we get the GPU accelerated performance.

If everything worked fine, your content should now scroll smoothly at 60 FPS across the screen. In fact, in many cases, scrolling like this gives smoother results than the default browser scroll. It also works nicely on mobile.

Of course basic scrolling merely scratches the surface of what you can do. I move the content using a CSS translate, but you can tie the values returned by the VirtualScroll callback to anything. You can make an animation that scales or rotates elements on scroll. You can add some bouncy or elastic effects to the animation. You can even use the VirtualScroll to move through space 3d in a WebGL or CSS3D animation or to scrub through a video or image sequence.

I used this technique on several projects in the past, including my own portfolio, the Tool of North America site we built last year and some other parallax scrolling sites we did at Tool. The VirtualScroll script is part of a small framework I use for all my productions.

Note: on the 'work' section of the Tool site we used horizontal scrolling. Horizontal scrolling is tricky, because some users on laptops and desktop computers have mouse wheels that only allow for vertical scrolling with the wheel. At first I wanted to track the vertical scrolling (i.e. e.deltaY) and use it to translate the content on the X axis instead of Y. However this doesn't work very well on mobile, where it's more natural to tap and drag in the horizontal direction if this is how the content moves. So the actual code detects the type of interaction used and tracks either the e.deltaX or e.deltaY depending on the situation. It's always good to think through all interactions like that!

I hope you find this technique useful or at least interesting. Thanks!

Back
More posts

Everyday3D is a blog by Bartek Drozdz

I started Everyday3d in 2007 with a focus web development. Over the years, I wrote about technology, graphics programming, Virtual Reality and 360 photography. In 2016, I co-founded Kuula - a virtual tour software and I work on it ever since.

Recently, I post about my travels and other topics that I am curious about. If you want to be notified about future posts, subscribe via Substack below.