Adaptive scrolling with jQuery

By now, most Front-end developers accustomed to jQuery know about $.fn.animate() (doc), and how it can help, for instance, to smoothly scroll to a specific position:

// Scrolling 500px down in 1s? Fairly simple.
    scrollTop: 500,

Although I am not using jQuery that often anymore, I recently faced a little challenge while working with legacy code, when the "end" reference of an animated scroll, as defined by scrollTop (in my example, 500px), had to change while scrolling. In my case, a #breadcrumb element would become "affixed" at some point during scrolling: it would receive position: fixed and stick at the top of the screen. For the breadcrumb was removed from normal flow, its now-missing height had to be taken into account so the scrolling animation would stop at the proper location. I basically needed a way to instruct the jQuery's animation "engine" its final reference had changed meanwhile the very animation was going on.

Fortunately, $.fn.animate exposes a very useful yet mostly unnoticed callback, step, which can help working around that kind of problem..

That step callback is to be fired on a regular basis, "for each animated property of each animated element". In the case of the scrollTop property though, it is going to be triggered on an irregular basis, depending on the rate of mouse-scroll events induced by the user scrolling the page.

One could alleviate that choppiness by using requestAnimationFrame (for instance), distributing scroll events over time. The duration / distance ratio (the animation's "speed") is also a constraint, so further manual tuning may be required. But for the simplest cases, and as long as the scrolling animation spans a decent height, the natural scroll events rate will be steady enough that everything smoothes out naturally.

step is passed two arguments: now and tween. The former has its value set to the current state of the animated property, at time of calling; for scrollTop, it would record the current offset from the top, initial position, in pixels. The latter is a Tween object.

Tween stands for "in-between". The Tween pattern helps with modeling shifting values: a gradient, time passing by, or… an offset during a scroll. Anything that can be animated and whose value is to be picked within a range (a morphism really) is a good candidate for being tracked with a Tween object. jQuery comes with its own implementation of the Tween pattern, but some others exist (for instance, sole/tween.js or jeremyckahn/shifty).

jQuery's Tween registers an end value at the beginning of the process. That value is used to compute the now value on each step, by applying an easing function. The end reference usually remains still, but nothing prevents you from editing it while the process is happening. The computation for now occurs just before calling any registered step callback (check the code). You may have already figured it out: simply altering the tween's end value in the step callback is all that is required to modify the scrolling's final reference (granted your animation will perform over more than one mouse scroll event… which should always be the case, otherwise you may want to avoid animating altogether).

Boilerplate code:

$('.target').click(function {
  $('html, body').animate({
    scrollTop: 500
  }, {
    duration: 1000,
    step: function(now, tween) {
      // Check whether tween.end is still the correct value;
      // if not, redefine it: tween.end = someValue; and the
      // animation will adjust.

Obviously, changing the value of tween.end may mess with the easing function, that is, with the perceived animation. If you know for a fact that you will edit tween.end only during a linear portion of the easing, either because the easing function is fully linear or because you time it well, it will not be a problem: no one will actually notice. Things can get a little trickier with non-linear easing functions, and once again, one may need to smooth the "editing" of tween.end over several step calls, an operation more challenging than it seems.

Jun 13, 20143 min readSummary: A little trick to change a scroll's end position… while scrolling.