I was searching for a good pure Javascript implementation for animated scrolling to a target on click. Surprisingly, my google-fu was only able to come up with implementations dependent on jQuery. Thus I was forced to come up with my own solution and thought that it would be worth sharing. You can test the animation by clicking the subscript1.

For the tl;dr people, I’ve included the full code at the end. For the rest, I’ll walk you through the source.

I started with the relation between the navigation link and the section of page to navigate to. The best solution seemed to be to bind the navigation link to the id of the section header. There is an added bonus to this aspect: if the user for some reason has no access to Javascript, the link will still guide them correctly. I think that’s a big plus for accessibility.

The HTML would look like this. Note how the a element’s href attribute shares the id of the heading element.

<nav>
    <a href="#example" class="scroll-link">
</nav>
<!-- other elements -->
<section>
    <h2 id="example">This is an example title</h2>
    <p>Section content</p>
</section>

Then we loop through the link elements to attach the click event listeners. I used an old-fashioned for loop.

var links = document.getElementsByClassName('scroll-link');
for (var i = 0; i < links.length; i++) {
    links[i].onclick = scroll;
}

The scroll() function doesn’t exist yet. That’s our next task!

function scroll(e) {
    e.preventDefault();
    var id = this.getAttribute('href').replace('#', '');
    var target = document.getElementById(id).getBoundingClientRect().top;
    animateScroll(target);
}

There’s nothing fancy here – most of the cool stuff happens in the animateScroll() function that we’ll soon get into. Before calling the function however, there are three things we must do.

  1. Prevent the default action of the click – that’s navigating straight to the target
  2. Get a reference to the target element. We strip the “#” from the beginning of the href and call the getElementById() function of the document object with the result
  3. Get the vertical displacement of the target element from the top. We pass this value to the animateScroll function

We’re now ready to move to the animateScroll function

function animateScroll(targetHeight) {
    targetHeight = document.body.scrollHeight - window.innerHeight > targetHeight + scrollY ? 
        targetHeight : document.body.scrollHeight - window.innerHeight;
    var initialPosition = window.scrollY;
    var SCROLL_DURATION = 30;
    var step_x = Math.PI / SCROLL_DURATION;
    var step_count = 0;
    requestAnimationFrame(step);
    function step() {
        if (step_count < SCROLL_DURATION) {
            requestAnimationFrame(step);
            step_count++;
            window.scrollTo(0, initialPosition + targetHeight * 0.25 * Math.pow((1 - Math.cos(step_x * ++step_count)), 2));
        }
    }
}

This one’s takes a bit more effort. At first glance we see that there’s a plenty of variable definitions and an inner helper function. I’ve split the function into smaller pieces below to deal with them one by one. Let’s start with the variables.

targetHeight = document.body.scrollHeight - window.innerHeight > targetHeight + scrollY ? 
    targetHeight : document.body.scrollHeight - window.innerHeight;
var initialPosition = window.scrollY;

On the first line we redefine the targetHeight parameter. The inline conditional looks a bit convoluted but what we do is basically pretty simple. We check if the target height is so close to the bottom of the page that it’s impossible to scroll so low that the target is at the top of the page. If that’s the case, we set the bottom of the page as the target.

After this we grab the current scroll position and set this to a variable. This is needed in the animation loop, as the user is not necessarily at the top of the page when clicking the link.

var SCROLL_DURATION = 30;
var step_x = Math.PI / SCROLL_DURATION;
var step_count = 0;

The step_x variables definition may seem a bit unexpected at first. What’s that pi doing in my Javascript? For a natural scrolling animation we’re going to need some trigonometry. The step_x variable will help us later when we get serious with the cosine function.

The step-count is used for – counting steps. We’ll be using the frame count as a cue for stopping the animation. The all-capital SCROLL_DURATION tells us the duration of the animation in frames. The all-caps signals that it’s a fixed constant value.

The step() function performs a single step of the animation. The line with the window.scrollTo() call is the actual page scrolling. In addition to that, we increment the step_counter, check if there are still frames left to animate and if so, well request another one, calling the function itself recursively.

function step() {
    if (step_count < SCROLL_DURATION) {
        requestAnimationFrame(step);
        step_count++;
        window.scrollTo(0, 
            initialPosition + 
            targetHeight * 0.25 * 
            Math.pow((1 - Math.cos(step_x * step_count)), 2));
    }
}

Let’s look at the math used in the y-coordinate of the scrollTo() call. First of all we have the initialPosition value that we saved earlier. Then we have a rather complicated looking calculation. It might be easier to understand in the form of an equation (you can also skip the math if it feels like too much)

y = y0 + 1/4 * h (1 - cos(sx * x) )2

What we’re trying to accomplish here is to progress the scrolling along a cosine squared curve. This makes for a nice smooth transition in the style of Bezier curve. The curve starts at initialPosition and ends at targetHeight, as it should.

There’s one final thing that hasn’t been dealt with yet and that just happens to be the actual animation. As you can see, there are no setTimeout() or setInterval() calls in the code. Instead we are using the requestAnimationFrame() function2 that’s so useful in web animation that it justifies the horribly long name.

The function is used in two places in the code. Right after defining our variables in animateScroll() we call it with the step() function as callback to initialize the animation. After this every animation loop recursively calls for another frame to be rendered.

And so, we have animation! Is there a lesson to be learned from this? To me, the implementation task was a reminder of how tricky animation can be with pure Javascript. There are some great animation libraries available and I recommend using one. Working through the oddities may be fun and educational but usually the right choice is to stick with the best libraries when it comes to web animation.


1. Yeah, this one is just for demonstration. Click me to get back.

2. The animation method also is the reason for the one inconsistency in the code. How long does it take for the scroll animation complete? As a matter of fact, that depends on your monitor. The `requestAnimationFrame()` function calls a new frame based on the refresh rate of the hardware. That means that depending on your hardware, the animation duration can be for example half a second (30 frames / 60 Hz = 0.5 seconds) or barely a quarter (30 frames / 120Hz = 0.25 seconds). There are ways to find out the framarate by for example measuring it from two consecutive calls to `requestAnimationFrame()`. That, however shall be left for future development. I may one day post an update with the functionality added but in the mean time I recommend it as an exercise for the reader.