Smooth Scrolling With JavaScript

A technical look at creating a library from scratch

Photo by Damon Lam on Unsplash (it’s a scroll. get it?)

Today, we will explore how smooth scrolling works on the web by building a smooth scrolling library from scratch that will have the following features:

  • zero dependencies
  • animations with cubic Bézier curves and easing presets — the most interesting part!
  • ability to scroll inside any element and not just window
  • specify the direction of scrolling
  • specify scroll amount in px (optional)
  • specify the duration over which the scroll will happen
  • callback to cancel the scrolling event at any point

The library will expose a function that will accept the different input parameters required like the element to scroll, the scroll amount, etc. as part of one object.

function smoothScroll(scrollParams = {}) {
const elementToScroll = scrollParams.element;
...
}

Detecting the element type

For simplicity’s sake, let’s assume that we want to scroll inside the element from left to right. The first task is to find out the type of element — if it’s window or not. This is because window, compared to other HTML elements, has different DOM APIs to calculate width, height, and manipulating scroll positions.

function smoothScroll(scrollParams) {
const elementToScroll = scrollParams.element;
const isWindow = elementToScroll === window;
...
}

Based on the type of element, we use appropriate properties, as seen below.

Detect how much to scroll

The next step is to calculate how much to scroll if the scroll amount is not specified in the parameters. Otherwise, we calculate it based on the width of the element and its initial scroll position.

Triggering the smooth scroll

Now, we need to start scrolling the element at a pace based on the duration provided in the parameters. A continuously self-executing function is provided to requestAnimationFrame as a callback. requestAnimationFrame is a non-blocking way to call a function that performs an animation just before each repaint cycle of the browser.

On each tick, that is, each invocation of the callback function, the function will calculate the amount that needs to be scrolled. This will depend on two interdependent factors:

  • time elapsed since the start
  • animation parameters specified, which will dictate the pace of the scrolling

Animations and timing functions

In CSS, we have the provision of defining the animations of some properties like background-color and opacity through:

  • easing presets (ease-in , ease-out ,ease-in-out etc.)
  • cubic Bézier curve points

Under the hood, both of these methods use the concept of timing functions.

A timing function is a function of time and defines the variation of speed of an animation over a given duration, that is, its acceleration.

You can read in depth about timing functions here.

Unfortunately, there is no out-of-the-box way to define the animation of a scroll. So, we’ll have to wire that up ourselves!

In the context of our problem, the timing function will take the ratio of the time elapsed and the total duration of the animation as input. For example, if the duration specified was 2s, and 0.5s have elapsed, then the input to the timing function would be 0.5 / 2 = 0.25.

The return value lies between 0 and 1, which defines what fraction of the total scroll amount the element has to be scrolled to. For example, if the return value is 0.50 and the total scroll amount is 500px, that means the element has to be scrolled to 50% of 500, which is 250px.

Let’s look at the timing functions of some easing presets:

To get more clarity, let’s take a preset, say easeOutQuad, and say we want to scroll a total amount of 200px over 2s. Here’s what the scroll position looks like at different points in time:

easeOutQuad dictates that an animation should start out fast and then become slow gradually,vas seen in the above table. The user can see the element being scrolled from 0 to 150px in the first second and then only 50px in the remaining second.

Cubic Bézier Curves

Easing presets are very specific and difficult to define. For example, easeInOutQuint, defined above, has a very complex formula but achieves only a very simple animation compared to cubic Bézier curves. For advanced customised animations, we require cubic Bézier curves that offer ease and ability to define complex acceleration patterns.

Image Source

Cubic Bézier curves for animations are defined in a 2D plane with the help of 4 points called control points — P0 (0, 0), P1, P2, P3(1, 1). The X axis specifies the time elapsed, and the Y axis tells us the progress percentage of the animation (in our case, what percent of the total scroll amount has been scrolled). You can (and should!) read more about Bézier curves here. Here’s a pen containing an interactive cubic Bézier curve implementation to get a feel of how they work.

The cubic Bézier function is a mathematical formula that takes percentage time elapsed, P1, and P2 control points as input and returns the percentage progress. In our case it will be translated to code as follows:

Now, let’s define the function that will return the animation percentage progress based on time elapsed.

Tick Function

Let’s understand what’s happening step by step.

  1. scrollOnNextTick is called for the first time, wrapped inside requestAnimationFrame. requestAnimationFrame provides the number of milliseconds elapsed since 1970 as a default argument, which we store in startTime, and is also the argument to scrollOnNextTick on each tick.
  2. runTime is calculated on each tick, which tells us how much time has elapsed since the animation started.
  3. getProgress takes runTime as an argument and returns the animation progress percentage (a value between 0 and 1), which is multiplied with the total scroll amount that needs to be scrolled, giving us the scroll amount that needs to be scrolled in this tick.
  4. The scroll position is calculated and set based on the initial scroll position and the scroll amount for this tick.
  5. If onRefUpdateCallback is supplied, it will be called on each tick. requestAnimationFrame(scrollOnNextTick) is passed as an argument that can be used to cancel the scroll animation by passing it to cancelAnimationFrame as an argument.
  6. If runTime becomes greater than duration, it means the animation is complete. An optional callback onAnimationCompleteCallback is called if supplied.

Usage

An example of how the smoothScroll function can be used:

Wrapping it up!

The final step is to expose the smoothScroll function to be used by applications. For this, few things need to be done:

  • export smoothScroll as the default function:
export default smoothScroll
  • Compile ES6 to ES5 for use in browsers since all browsers can’t understand ES6 completely. For this, we can use any bundler (eg: Webpack).
  • (optional) An npm package can be created so that our library is npm installable.

You can check out the entire code in the following links:

GitHub: https://github.com/tarun-dugar/easy-scroll 
npm: https://www.npmjs.com/package/easy-scroll