Skip to content

Instantly share code, notes, and snippets.

@felipenmoura
Forked from joshbeckman/animatedScrollTo.js
Last active November 30, 2022 10:01
Show Gist options
  • Star 24 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save felipenmoura/650e7e1292c1e7638bcf6c9f9aeb9dd5 to your computer and use it in GitHub Desktop.
Save felipenmoura/650e7e1292c1e7638bcf6c9f9aeb9dd5 to your computer and use it in GitHub Desktop.
ScrollTo animation using pure javascript and no jquery
/**
* Will gracefuly scroll the page
* This function will scroll the page using
* an `ease-in-out` effect.
*
* You can use it to scroll to a given element, as well.
* To do so, pass the element instead of a number as the position.
* Optionally, you can pass a `queryString` for an element selector.
*
* The default duration is half a second (500ms)
*
* This function returns a Promise that resolves as soon
* as it has finished scrolling. If a selector is passed and
* the element is not present in the page, it will reject.
*
* EXAMPLES:
*
* ```js
* window.scrollPageTo('#some-section', 2000);
* window.scrollPageTo(document.getElementById('some-section'), 1000);
* window.scrollPageTo(500); // will scroll to 500px in 500ms
* ```
*
* @returns {Promise}
* @param {HTMLElement|Number|Selector} Target
* @param {Number} Duration [default=500]
*
* Inspired by @andjosh's work
*
*/
function scrollPageTo (to, duration=500) {
//t = current time
//b = start value
//c = change in value
//d = duration
const easeInOutQuad = function (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 new Promise((resolve, reject) => {
const element = document.scrollingElement;
if (typeof to === 'string') {
to = document.querySelector(to) || reject();
}
if (typeof to !== 'number') {
to = to.getBoundingClientRect().top + element.scrollTop;
}
let start = element.scrollTop,
change = to - start,
currentTime = 0,
increment = 20;
const animateScroll = function() {
currentTime += increment;
let val = easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if(currentTime < duration) {
setTimeout(animateScroll, increment);
} else {
resolve();
}
};
animateScroll();
});
}
@felipenmoura
Copy link
Author

The benefits:

  • It returns a promise that resolves when the animation is done
  • It accepts an element as coordinate and scrolls to it (also works with a selector like #some-section-id)
  • It will not overwrite any existing public structure (like the scrollTo native function or Math's prototype)
  • Has a default duration
  • It also uses the document.scrollingElement and you don't need to send document, document.body, window or document.documentElement according to your page structure

@drozumek
Copy link

GREAT Job. I would use cubic bezier function instead of quad
http://www.gizma.com/easing/#cub3
to me looks better.

@audunolsen
Copy link

Good stuff. Without having examined your code thoroughly, I see no repercussions when swapping setTimeout(animateScroll, increment); for requestAnimationFrame(animateScroll);. That's the only potential for improvement which I see.

@tyleryoungblood
Copy link

Fixed a typo (ease-in-out on line 4) but since I can't create a pull request for your gist, I'll link mine here.
https://gist.github.com/tyleryoungblood/10a8084bf5f166ed7fe33b0436bbd565

@Gillesvk
Copy link

Gillesvk commented Oct 7, 2019

In my case, the easing does not seem to work when scroll to a seclector (like #footer).

EDIT :

Here is a very simple CodePen example. The easing is not working when scroll down to the footer. It's like the easing doesn't have the time to be completed.

RE-EDIT :

I get the solution by myself.

This is because the bottom of the page can't scroll to the top of the window (of course!). So, when smooth scroll to the bottom of the page, to get an easing (or a duration) that matches the position I have to use:

var position = document.body.clientHeight - window.innerHeight;

and write the click binding like this:

document.querySelector(".scroll-bottom").onclick = function () {
   var position = document.body.clientHeight - window.innerHeight;
   window.scrollPageTo(position);
}

The above CodePen example is working well now.

@karunt
Copy link

karunt commented Apr 4, 2020

The benefits:

It returns a promise that resolves when the animation is done
It accepts an element as coordinate and scrolls to it (also works with a selector like #some-section-id)
It will not overwrite any existing public structure (like the scrollTo native function or Math's prototype)
Has a default duration
It also uses the document.scrollingElement and you don't need to send document, document.body, window or document.documentElement according to your page structure

Thanks for sharing. Any idea on how to implement it in Angular 7? I've included its reference to angular.json in the scripts array, but don't know how to include the function scrollPageTo in a component.

@9mm
Copy link

9mm commented Aug 27, 2021

An updated version that satisfies much more strict ESLint parameters, plus new ES syntax:

export const animateScrollTo = (to, duration) => {
  const element = document.scrollingElement || document.documentElement;
  const start = element.scrollTop;
  const change = to - start;
  const startDate = +new Date();
  // t = current time
  // b = start value
  // c = change in value
  // d = duration
  const easeInOutQuad = (t, b, c, d) => {
    let t2 = t;
    t2 /= d / 2;
    if (t2 < 1) return (c / 2) * t2 * t2 + b;
    t2 -= 1;
    return (-c / 2) * (t2 * (t2 - 2) - 1) + b;
  };
  const animateScroll = () => {
    const currentDate = +new Date();
    const currentTime = currentDate - startDate;
    element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration), 10);
    if (currentTime < duration) {
      requestAnimationFrame(animateScroll);
    } else {
      element.scrollTop = to;
    }
  };
  animateScroll();
};

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