Thursday, May 5, 2016

How to Implement Smooth Scrolling in Vanilla JavaScript

Smooth scrolling is a user interface pattern that progressively enhances the default in-page navigation experience, animating the change of position within the scroll box (the viewport, or a scrollable element) from the location of the activated link to the location of the destination element indicated in the hash fragment of the link URL.

This is nothing new, being a pattern known from many years now, check for instance this SitePoint article that dates back to 2003! As an aside, this article has a historical value as it shows how client-side JavaScript programming, and the DOM in particular, has changed and evolved over the years, allowing the development of less cumbersome vanilla JavaScript solutions.

There are many implementations of this pattern within the jQuery ecosystem, either using jQuery directly or implemented with a plugin, but in this article we are interested in a pure JavaScript solution. Specifically, we are going to explore and leverage the Jump.js library.

After a presentation of the library, with an overview of its features and characteristics, we will apply some changes to the original code to adapt it to our needs. In doing this, we will refresh some core JavaScript language skills such as functions and closures. We will then create a HTML page to test the smooth scrolling behavior that it will be then implemented as a custom script. Support, when available, for native smooth scrolling with CSS will then be added and finally we will conclude with some observations concerning the browser navigation history.

Here is a is the final demo that we'll be creating:

See the Pen Smooth Scrolling by SitePoint (@SitePoint) on CodePen.

The full source code is available on GitHub.

Jump.js

Jump.js is written in vanilla ES6 JavaScript, without any external dependencies. It is a small utility, being only about 42 SLOC, but the size of the provided minified bundle is around 2.67 KB because it has to be transpiled. A Demo is available on the GitHub project page.

As suggested by the library name, it provides only the jump: the animated change of the scrollbar position from its current value to the destination, specified by providing either a DOM element, a CSS selector, or a distance in the form of a positive or negative number value. This means that in the implementation of the smooth scrolling pattern, we must perform the link hijacking ourselves. More on this in the following sections.

Note that currently only the scrolling of the viewport is supported and only vertically.

We can configure a jump with some options such as the duration (this parameter is mandatory), the easing function, and a callback to be fired at the end of the animation. We will see them in action later in the demo. See the documentation for full details.

Jump.js runs without problems on 'modern' browsers, including Internet Explorer version 10 or higher. Again, refer to the documentation for the full list of supported browsers. With a suitable polyfill for requestAnimationFrame it should run even on older browsers.

A Quick Peek Behind the Screen

Internally the Jump.js source uses the requestAnimationFrame method of the window object to schedule the update of the position of the viewport vertical position at each frame of the scrolling animation. This update is achieved passing the next position value computed with the easing function to the window.scrollTo method. See the source for full details.

A Bit of Customization

Before diving into a demo to show the usage of Jump.js, we are going to make some slight changes to the original code, that will however leave its inner workings unmodified.

The source code is written in ES6 and needs to be used with a JavaScript build tool for transpiling and bundling modules. This could be overkill for some projects, so we are going to apply some refactoring to convert the code to ES5, ready to be used everywhere.

First things first, let's remove the ES6 syntax and features. The script defines an ES6 class:

import easeInOutQuad from './easing'

export default class Jump {
  jump(target, options = {}) {
    this.start = window.pageYOffset

    this.options = {
      duration: options.duration,
      offset: options.offset || 0,
      callback: options.callback,
      easing: options.easing || easeInOutQuad
    }

    this.distance = typeof target === 'string'
      ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
      : target

    this.duration = typeof this.options.duration === 'function'
      ? this.options.duration(this.distance)
      : this.options.duration

    requestAnimationFrame(time => this._loop(time))
  }

  _loop(time) {
    if(!this.timeStart) {
      this.timeStart = time
    }

    this.timeElapsed = time - this.timeStart
    this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

    window.scrollTo(0, this.next)

    this.timeElapsed < this.duration
      ? requestAnimationFrame(time => this._loop(time))
      : this._end()
  }

  _end() {
    window.scrollTo(0, this.start + this.distance)

    typeof this.options.callback === 'function' && this.options.callback()
    this.timeStart = false
  }
}

We could convert this to a ES5 'class' with a constructor function and a bunch of prototype methods, but observe that we never need multiple instances of this class, so a singleton implemented with a plain object literal will do the trick:

var jump = (function() {

    var o = {

        jump: function(target, options) {
            this.start = window.pageYOffset

            this.options = {
              duration: options.duration,
              offset: options.offset || 0,
              callback: options.callback,
              easing: options.easing || easeInOutQuad
            }

            this.distance = typeof target === 'string'
              ? this.options.offset + document.querySelector(target).getBoundingClientRect().top
              : target

            this.duration = typeof this.options.duration === 'function'
              ? this.options.duration(this.distance)
              : this.options.duration

            requestAnimationFrame(_loop)
        },

        _loop: function(time) {
            if(!this.timeStart) {
              this.timeStart = time
            }

            this.timeElapsed = time - this.timeStart
            this.next = this.options.easing(this.timeElapsed, this.start, this.distance, this.duration)

            window.scrollTo(0, this.next)

            this.timeElapsed < this.duration
              ? requestAnimationFrame(_loop)
              : this._end()
        },

        _end: function() {
            window.scrollTo(0, this.start + this.distance)

            typeof this.options.callback === 'function' && this.options.callback()
            this.timeStart = false
        }

    };

    var _loop = o._loop.bind(o);

    // Robert Penner's easeInOutQuad - http://ift.tt/1mCMeMf
    function easeInOutQuad(t, b, c, d)  {
        t /= d / 2
        if(t < 1) return c / 2 * t * t + b
        t--
        return -c / 2 * (t * (t - 2) - 1) + b
    }

    return o;

})();

Apart from removing the class, we needed to make a couple of other changes. The callback for requestAnimationFrame, used to update the scrollbar position at each frame, which in the original code is invoked through an ES6 arrow function, is pre-bound to the jump singleton at init time. Then we bundle the default easing function in the same source file. Finally, we have wrapped the code in an IIFE(Immediately-invoked Function Expressions) to avoid namespace pollution.

Continue reading %How to Implement Smooth Scrolling in Vanilla JavaScript%


by Giulio Mainardi via SitePoint

No comments:

Post a Comment