Skip to content

Instantly share code, notes, and snippets.

@dezinezync
Last active January 28, 2021 09:17
Show Gist options
  • Save dezinezync/5487119 to your computer and use it in GitHub Desktop.
Save dezinezync/5487119 to your computer and use it in GitHub Desktop.
Vanilla Javascript Scrolling function for easing based scrolling
function scrollTo(Y, duration, easingFunction, callback) {
var start = Date.now(),
elem = document.documentElement.scrollTop?document.documentElement:document.body,
from = elem.scrollTop;
if(from === Y) {
callback();
return; /* Prevent scrolling to the Y point if already there */
}
function min(a,b) {
return a<b?a:b;
}
function scroll(timestamp) {
var currentTime = Date.now(),
time = min(1, ((currentTime - start) / duration)),
easedT = easingFunction(time);
elem.scrollTop = (easedT * (Y - from)) + from;
if(time < 1) requestAnimationFrame(scroll);
else
if(callback) callback();
}
requestAnimationFrame(scroll)
}
/* bits and bytes of the scrollTo function inspired by the works of Benjamin DeCock */
/*
* Easing Functions - inspired from http://gizma.com/easing/
* only considering the t value for the range [0, 1] => [0, 1]
*/
var easing = {
// no easing, no acceleration
linear: function (t) { return t },
// accelerating from zero velocity
easeInQuad: function (t) { return t*t },
// decelerating to zero velocity
easeOutQuad: function (t) { return t*(2-t) },
// acceleration until halfway, then deceleration
easeInOutQuad: function (t) { return t<.5 ? 2*t*t : -1+(4-2*t)*t },
// accelerating from zero velocity
easeInCubic: function (t) { return t*t*t },
// decelerating to zero velocity
easeOutCubic: function (t) { return (--t)*t*t+1 },
// acceleration until halfway, then deceleration
easeInOutCubic: function (t) { return t<.5 ? 4*t*t*t : (t-1)*(2*t-2)*(2*t-2)+1 },
// accelerating from zero velocity
easeInQuart: function (t) { return t*t*t*t },
// decelerating to zero velocity
easeOutQuart: function (t) { return 1-(--t)*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuart: function (t) { return t<.5 ? 8*t*t*t*t : 1-8*(--t)*t*t*t },
// accelerating from zero velocity
easeInQuint: function (t) { return t*t*t*t*t },
// decelerating to zero velocity
easeOutQuint: function (t) { return 1+(--t)*t*t*t*t },
// acceleration until halfway, then deceleration
easeInOutQuint: function (t) { return t<.5 ? 16*t*t*t*t*t : 1+16*(--t)*t*t*t*t }
}
/* Add the following to you main js file
// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller
// fixes from Paul Irish and Tino Zijdel
(function() {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
|| window[vendors[x]+'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame)
window.requestAnimationFrame = function(callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function() { callback(currTime + timeToCall); },
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
if (!window.cancelAnimationFrame)
window.cancelAnimationFrame = function(id) {
clearTimeout(id);
};
}()); */
@alexserver
Copy link

May I have an example of how to use it ?

@GrzegorzWidla
Copy link

@alexserver - in case you or somebody else hasn't figured it out:

scrollTo(0, 800, easing.easeInOutCubic);

Where:

  • 0 is Y position in pixels; 0 specifically means top of the window,
  • 800 is duration of the animation in miliseconds.

You can also add a callback to do something after animation is done:

scrollTo(320, 600, easing.linear, function() { 
    alert('scrolled');  
}

@dennisbaum
Copy link

if no callback is definded, and i reload the page, i get an error in line 8
if from === Y

i replaced it with
if(from === Y) {
if(callback) callback();
return; /* Prevent scrolling to the Y point if already there */
}

and tnx for this nice script!

@cybergrunge
Copy link

Hi, with Firefox (35, Developer Edition), I can't manage to scroll if the current scroll position is on top of the page. Any idea ?

@Eyal-Shalev
Copy link

I've forked Nikhil's scroll.easing.js to https://gist.github.com/Eyal-Shalev/24fa303cc98ee1ea4b4a.

Usage example:
var SomeHTMLContainer = document.getElementById('some-scroll-container');
var scrollTime = 1000;
var someCallbackFunction = function() {console.log('hello');};
var scroller = new Scroller(SomeHTMLContainer, scrollTime)
.easing(Scroller.EASING.easeInQuad)
.callback(someCallbackFunction);

var top = 50, left = 150;
scroller.to(top,left);

@grayghostvisuals
Copy link

Here's my example and keeping it Vanilla still (no jQuery). Also to set the scene I placed the event listener on the ul that contains lis with my anchors inside containing the hash values.

<h1 id="table-of-contents">Table of Contents</h1>
<ul>
  <li><a href="#about-the-author">About The Author</a></li>
  <li><a href="#contributing">Contributing</a></li>
  <li><a href="#about-sass">About Sass</a>
    <ul>
      <li><a href="#ruby-sass-or-libsass">Ruby Sass Or LibSass</a></li>
      <li><a href="#sass-or-scss">Sass Or SCSS</a></li>
      <li><a href="#other-preprocessors">Other Preprocessors</a></li>
    </ul>
  </li>
  <li><a href="#introduction">Introduction</a>
    <ul>
      <li><a href="#why-a-styleguide">Why A Styleguide</a></li>
      <li><a href="#disclaimer">Disclaimer</a></li>
      <li><a href="#key-principles">Key Principles</a></li>
    </ul>
  </li>
</ul>
var sgl_toc_list = document.getElementById('table-of-contents').nextElementSibling; // grab the ul wth the links inside it

function sglScrollmation(event) {
  var scroll_speed = 800,
      hash_value   = event.target.attributes.href.value.substring(1), // remove the # from the returned value
      hash_section = document.getElementById(hash_value).offsetTop,
      ease_motion  = easing.easeInOutCubic;

  scrollTo(hash_section, scroll_speed, ease_motion);
}

sgl_toc_list.addEventListener('click', sglScrollmation);

@azizur
Copy link

azizur commented Feb 9, 2015

@dezinezync would be kind enough to add some information around under which license is this code being made public? Specifically the easing functions as they are quite different than those found at http://gizma.com/easing/.

The reason I am asking because without a specified license its difficult to use this code in some projects. Also would like to give attribution to the author properly.

@Polaris72
Copy link

First: this script is really good, works nice with little payload, well done!
But: When using addEventListener, like grayghostvisuals does, the browser performs the "normal" anchor-jump for a split second and then the nice scrolling takes place, via the script as it is supposed to be.

Any ideas how to prevent the first browser-scroll? I tried "onclick" on the container, instead of addEventListener, didn't work at all.

p.s.
firebug reported an "easedT = easingFunction(time); is not a function"-error on loading a page, so i added:

if (easingFunction)
{
    var currentTime = Date.now(),
    time = min(1, ((currentTime - start) / duration)),
    easedT = easingFunction(time);

    elem.scrollTop = (easedT * (Y - from)) + from;

    if(time < 1) requestAnimationFrame(scroll);
    else if(callback) callback();
}

to function "scroll" to prevent this, which does not affect my primary issue.

@deanwagman
Copy link

There seems to be a bug to where the document will not scroll if scrollTop is 0 in some browsers. And some browsers will always report 0 for scrollTop. Therefore, I've found a patch for line 4.

elem = (document.documentElement.scrollTop++ !== 0) ? document.documentElement : document.body,

This attempts to increment the position to see if changing it does anything. If it does, then use it, else use document.body.

Whew…

@deanwagman
Copy link

Turns out that wasn't a good fix either. Firefox didn't like the incrementer.

Instead you should set the scrollTop to itself plus one, then check it.

Like so:

                // Used to see which object we should scroll on
        document.documentElement.scrollTop = document.documentElement.scrollTop + 1;

        var start = Date.now(),
            elem = (document.documentElement.scrollTop !== 0) ? document.documentElement : document.body,
            from = elem.scrollTop,
...

Hope that helps.

@marcobiedermann
Copy link

Why not use the build it function Math.min on line 19 instead of creating your own function at line 12?

time = Math.min(1, ((currentTime - start) / duration)),

@pawelgrzybek
Copy link

This gist was very helpful for me. Because we ended up a bit confused about element detection I put all the things together.

function scrollIt(destination, duration = 200, easing = 'linear', callback) {
  // define timing functions
  let easings = {
    // no easing, no acceleration
    linear(t) {
      return t;
    },
    // accelerating from zero velocity
    easeInQuad(t) {
      return t * t;
    },
    // decelerating to zero velocity
    easeOutQuad(t) {
      return t * (2 - t);
    },
    // acceleration until halfway, then deceleration
    easeInOutQuad(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },
    // accelerating from zero velocity
    easeInCubic(t) {
      return t * t * t;
    },
    // decelerating to zero velocity
    easeOutCubic(t) {
      return (--t) * t * t + 1;
    },
    // acceleration until halfway, then deceleration
    easeInOutCubic(t) {
      return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
    },
    // accelerating from zero velocity
    easeInQuart(t) {
      return t * t * t * t;
    },
    // decelerating to zero velocity
    easeOutQuart(t) {
      return 1 - (--t) * t * t * t;
    },
    // acceleration until halfway, then deceleration
    easeInOutQuart(t) {
      return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
    },
    // accelerating from zero velocity
    easeInQuint(t) {
      return t * t * t * t * t;
    },
    // decelerating to zero velocity
    easeOutQuint(t) {
      return 1 + (--t) * t * t * t * t;
    },
    // acceleration until halfway, then deceleration
    easeInOutQuint(t) {
      return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
    }
  };

  function checkElement() {
    // returns document.documentElement for chrome and safari
    // document.body for rest of the world
    document.documentElement.scrollTop += 1;
    let elm = (document.documentElement.scrollTop !== 0) ? document.documentElement : document.body;
    document.documentElement.scrollTop -= 1;
    return elm;
  }

  let element = checkElement();
  let start = element.scrollTop;
  let startTime = Date.now();

  function scroll() {
    let now = Date.now();
    let time = Math.min(1, ((now - startTime) / duration));
    let timeFunction = easings[easing](time);
    element.scrollTop = (timeFunction * (destination - start)) + start;

    if (element.scrollTop === destination) {
      callback;
      return;
    }
    requestAnimationFrame(scroll);
  }
  scroll();
}

And this is how to use it:

scrollIt(1000, 300, 'easeInQuad', console.log('done'));

or simple version with default arguments if you don't care about duration, easing and callback...

scrollIt(1000);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment