Skip to content

Instantly share code, notes, and snippets.

@joshbeckman
Created September 30, 2013 14:51
Show Gist options
  • Select an option

  • Save joshbeckman/6764939 to your computer and use it in GitHub Desktop.

Select an option

Save joshbeckman/6764939 to your computer and use it in GitHub Desktop.
ScrollTo animation using pure javascript and no jquery
document.getElementsByTagName('button')[0].onclick = function () {
scrollTo(document.body, 0, 1250);
}
function scrollTo(element, to, duration) {
var start = element.scrollTop,
change = to - start,
currentTime = 0,
increment = 20;
var animateScroll = function(){
currentTime += increment;
var val = Math.easeInOutQuad(currentTime, start, change, duration);
element.scrollTop = val;
if(currentTime < duration) {
setTimeout(animateScroll, increment);
}
};
animateScroll();
}
//t = current time
//b = start value
//c = change in value
//d = duration
Math.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;
};
@AdaltonLeite

Copy link
Copy Markdown

Thank you very much! 😄

@Cdvalencia

Cdvalencia commented Aug 22, 2018

Copy link
Copy Markdown

thanks, this example used it, with react.js, so:

constructor(props){
   super(props);
   this.scrollTo = this.scrollTo.bind(this);
 }; 

 scrollDown(num) {    
   console.log(num)
   this.scrollTo(document.body, num, 1250);
 }

 scrollTo(element, to, duration) {
     var start = document.scrollingElement.scrollTop,
         change = to - start,
         currentTime = 0,
         increment = 20;

     var animateScroll = function(){
         var 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;
         };
         currentTime += increment;
         var val = easeInOutQuad(currentTime, start, change, duration);
         document.scrollingElement.scrollTop = val;
         if(currentTime < duration) {
             setTimeout(animateScroll, increment);
         }
     };
     animateScroll();
 }

@hugotox

hugotox commented Sep 5, 2018

Copy link
Copy Markdown

Just curious, why mutate the global Math object?

@tomaswolfgang

tomaswolfgang commented Sep 6, 2018

Copy link
Copy Markdown

If you don't want to deal with any sort of intense implementation and you don't care about the easing function, duration, animation speed or any particulars:
scrollTo(ypos){ window.scrollTo({ top: ypos, behavior: "smooth" }) }
This should do the trick

@vinczebalazs

Copy link
Copy Markdown

Thanks so much, great solution! :)

@c-emil

c-emil commented Sep 18, 2018

Copy link
Copy Markdown

@tomaswolfgang window.scrollTo is very nice solution as long as you need to scroll window. It's unfortunately not usable for any element on the page.

@DenysMorozov

Copy link
Copy Markdown

@tomaswolfgang window.scrollTo is very nice solution as long as you need to scroll window. It's unfortunately not usable for any element on the page.

const scrolEl = document.querySelector('.modal-window-container');
const x = elX - (scrolEl.clientWidth / 2);
const y = elY - (scrolEl.clientHeight / 2);
scrolEl.scrollTo(x, y);

Everything works fine for us.

@whossein

Copy link
Copy Markdown
const
scrollTo = function(to, duration) {
    const
    element = document.scrollingElement || document.documentElement,
    start = element.scrollTop,
    change = to - start,
    startDate = +new Date(),
    // t = current time
    // b = start value
    // c = change in value
    // d = duration
    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;
    },
    animateScroll = function() {
        const currentDate = +new Date();
        const currentTime = currentDate - startDate;
        element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration));
        if(currentTime < duration) {
            requestAnimationFrame(animateScroll);
        }
        else {
            element.scrollTop = to;
        }
    };
    animateScroll();
};

Here's the code a bit modernized. Now it's a lot smoother and works in Safari as well. If you don't work with babel then replace const with var.

thanks very much!

@ClausClaus

Copy link
Copy Markdown

Thanks!!

@felipenmoura

Copy link
Copy Markdown

Very nice.

I forked it and made a few changes:
https://gist.github.com/felipenmoura/650e7e1292c1e7638bcf6c9f9aeb9dd5

  • 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

I hope it helps and thanks for both inspiring it making it public ;)

@sundayhd

Copy link
Copy Markdown

function scrollTo(element, to = 0, duration= 1000, scrollToDone = null) {
    const start = element.scrollTop;
    const change = to - start;
    const increment = 20;
    let currentTime = 0;

    const animateScroll = (() => {

      currentTime += increment;

      const val = Math.easeInOutQuad(currentTime, start, change, duration);

      element.scrollTop = val;

      if (currentTime < duration) {
        setTimeout(animateScroll, increment);
	} else {
		if (scrollToDone) scrollToDone();
	}
    });

    animateScroll();
  };

  Math.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;
  };

Thanks guys!! I added "scrollToDone" to execute callback function when scrolling done.

usage example:
scrollTo(document.body, 500, 1000, () => { console.log("Done with scrolling !!!!") }

@ruucm

ruucm commented May 6, 2019

Copy link
Copy Markdown

Thanks a lot!

@marsderp

marsderp commented May 9, 2019

Copy link
Copy Markdown

Has anyone added more easing functions?

@orrybaram

orrybaram commented Jun 18, 2019

Copy link
Copy Markdown

Here's a typescript version if anyone is interested:

type EaseInOutQuadOptions = {
  currentTime: number;
  start: number;
  change: number;
  duration: number;
};

const easeInOutQuad = ({
  currentTime,
  start,
  change,
  duration,
}: EaseInOutQuadOptions) => {
  let newCurrentTime = currentTime;
  newCurrentTime /= duration / 2;

  if (newCurrentTime < 1) {
    return (change / 2) * newCurrentTime * newCurrentTime + start;
  }

  newCurrentTime -= 1;
  return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};

type SmoothScrollOptions = {
  duration: number;
  element: HTMLElement;
  to: number;
};
export default function smoothScroll({
  duration,
  element,
  to,
}: SmoothScrollOptions) {
  const start = element.scrollTop;
  const change = to - start;
  const startDate = new Date().getTime();

  const animateScroll = () => {
    const currentDate = new Date().getTime();
    const currentTime = currentDate - startDate;
    element.scrollTop = easeInOutQuad({
      currentTime,
      start,
      change,
      duration,
    });

    if (currentTime < duration) {
      requestAnimationFrame(animateScroll);
    } else {
      element.scrollTop = to;
    }
  };
  animateScroll();
}

@wnsgh78b

wnsgh78b commented Aug 21, 2019

Copy link
Copy Markdown
	function scrollTo(element, durationTop, durationLeft) {
	    var startTop = element.scrollTop,
	    	startLeft = element.scrollLeft,
	        changeTop = durationTop - startTop,
	        changeLeft = durationLeft - startLeft,//window.scrollX - startLeft,
	        currentTimeTop = 0,
	        currentTimeLeft = 0,
	        increment = 20;
	       
	    var animateScrollTop = function(){        
	        currentTimeTop += increment;
	        var val = Math.easeInOutQuad(currentTimeTop, startTop, changeTop, durationTop);
	        element.scrollTop = val;
	        if(currentTimeTop < durationTop) {
	            setTimeout(animateScrollTop, increment);
	        }
		};

		var animateScrollLeft = function(){        
	        currentTimeLeft += increment;
	        var val = Math.easeInOutQuad(currentTimeLeft, startLeft, changeLeft, durationLeft);
	        element.scrollLeft = val;
	        if(currentTimeLeft < durationLeft) {
	            setTimeout(animateScrollLeft, increment);
	        }
		};

		animateScrollTop();
		animateScrollLeft();
	}

//t = current time
//b = start value
//c = change in value
//d = duration
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2tt + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};

@darkhorse-coder

darkhorse-coder commented Nov 20, 2019

Copy link
Copy Markdown

@andjosh Thank you!! Very useful!!

@joloiuhj

Copy link
Copy Markdown

@friedboats

Copy link
Copy Markdown

Thank you for this!!! Spent all day looking for a solution.

@mohsenasm

Copy link
Copy Markdown

Thanks!!!
دعای خیر جهانیان پشت سرت :)))

@emanavas

Copy link
Copy Markdown

Works Great. Thanks!!

@RaymondBakker

Copy link
Copy Markdown

Just a heads up that supplying 0 duration will make Math.easeInOutQuad() return the value -Infinity and supplying 1 duration will make it return large negative numbers. If you need to supply 0 duration, an easy fix would be to add an extra if statement at the start of this function returning the original supplied duration.

Math.easeInOutQuad = function (t, b, c, d) {
    if (d <= 0)
        return c;

...

}

Of course, the better way may be to just set scrollTop instead of using this function altogether.

@quirozcarlos

Copy link
Copy Markdown

Nice resource, is very useful 🥇

@KarloZKvasin

KarloZKvasin commented Nov 4, 2020

Copy link
Copy Markdown

Here's a typescript version if anyone is interested:

type EaseInOutQuadOptions = {
  currentTime: number;
  start: number;
  change: number;
  duration: number;
};

const easeInOutQuad = ({
  currentTime,
  start,
  change,
  duration,
}: EaseInOutQuadOptions) => {
  let newCurrentTime = currentTime;
  newCurrentTime /= duration / 2;

  if (newCurrentTime < 1) {
    return (change / 2) * newCurrentTime * newCurrentTime + start;
  }

  newCurrentTime -= 1;
  return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};

type SmoothScrollOptions = {
  duration: number;
  element: HTMLElement;
  to: number;
};
export default function smoothScroll({
  duration,
  element,
  to,
}: SmoothScrollOptions) {
  const start = element.scrollTop;
  const change = to - start;
  const startDate = new Date().getTime();

  const animateScroll = () => {
    const currentDate = new Date().getTime();
    const currentTime = currentDate - startDate;
    element.scrollTop = easeInOutQuad({
      currentTime,
      start,
      change,
      duration,
    });

    if (currentTime < duration) {
      requestAnimationFrame(animateScroll);
    } else {
      element.scrollTop = to;
    }
  };
  animateScroll();
}

Small update... and extend about direction / type

interface EaseInOutQuadOptions {
  currentTime: number;
  start: number;
  change: number;
  duration: number;
}

const easeInOutQuad = (currentTime, start, change, duration): EaseInOutQuadOptions => {
  let newCurrentTime = currentTime;
  newCurrentTime /= duration / 2;

  if (newCurrentTime < 1) {
    return (change / 2) * newCurrentTime * newCurrentTime + start;
  }

  newCurrentTime -= 1;
  return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};

interface SmoothScrollOptions {
  duration: number;
  element: HTMLElement;
  to: number;
  type: 'scrollTop' | 'scrollLeft';
}

const smoothScroll = (duration, element, to, type = 'scrollTop'): SmoothScrollOptions => {
  const start = element[type];
  const change = to - start;
  const startDate = new Date().getTime();

  const animateScroll = () => {
    const currentDate = new Date().getTime();
    const currentTime = currentDate - startDate;
    element[type] = easeInOutQuad(currentTime, start, change, duration);

    if (currentTime < duration) {
      requestAnimationFrame(animateScroll);
    } else {
      element[type] = to;
    }
  };
  animateScroll();

  return null;
};

export { smoothScroll };

@dhovart

dhovart commented Dec 17, 2020

Copy link
Copy Markdown

@KarloZKvasin I think your update is not properly typed. easeInOutQuad is set to return an EaseInOutQuadOptions. Same goes for smoothScroll, set to return a SmoothScrollOptions.
You should dump these interfaces and type each function argument instead.

Updated:

const easeInOutQuad = (
  currentTime: number,
  start: number,
  change: number,
  duration: number,
): number => {
  let newCurrentTime = currentTime;
  newCurrentTime /= duration / 2;

  if (newCurrentTime < 1) {
    return (change / 2) * newCurrentTime * newCurrentTime + start;
  }

  newCurrentTime -= 1;
  return (-change / 2) * (newCurrentTime * (newCurrentTime - 2) - 1) + start;
};

const smoothScroll = (
  duration: number,
  element: HTMLElement,
  to: number,
  property: 'scrollTop' | 'scrollLeft',
): void => {
  const start = element[property];
  const change = to - start;
  const startDate = new Date().getTime();

  const animateScroll = () => {
    const currentDate = new Date().getTime();
    const currentTime = currentDate - startDate;

    element[property] = easeInOutQuad(currentTime, start, change, duration);

    if (currentTime < duration) {
      requestAnimationFrame(animateScroll);
    } else {
      element[property] = to;
    }
  };
  animateScroll();
};

export { smoothScroll };

@aLIEzsss4

Copy link
Copy Markdown

how do work with scroll-snap-type: x mandatory;

@ruucm

ruucm commented Feb 17, 2021

Copy link
Copy Markdown
const
scrollTo = function(to, duration) {
    const
    element = document.scrollingElement || document.documentElement,
    start = element.scrollTop,
    change = to - start,
    startDate = +new Date(),
    // t = current time
    // b = start value
    // c = change in value
    // d = duration
    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;
    },
    animateScroll = function() {
        const currentDate = +new Date();
        const currentTime = currentDate - startDate;
        element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration));
        if(currentTime < duration) {
            requestAnimationFrame(animateScroll);
        }
        else {
            element.scrollTop = to;
        }
    };
    animateScroll();
};

Here's the code a bit modernized. Now it's a lot smoother and works in Safari as well. If you don't work with babel then replace const with var.

Thanks. it is a lot more soomother

@Ciantic

Ciantic commented Mar 17, 2021

Copy link
Copy Markdown

I also changed it to use performance.now():

var scrollTo = function(to, duration) {
    var element = document.scrollingElement || document.documentElement,
    start = element.scrollTop,
    change = to - start,
    startTs = performance.now(),
    // t = current time
    // b = start value
    // c = change in value
    // d = duration
    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;
    },
    animateScroll = function(ts) {
        var currentTime = ts - startTs;
        element.scrollTop = parseInt(easeInOutQuad(currentTime, start, change, duration));
        if(currentTime < duration) {
            requestAnimationFrame(animateScroll);
        }
        else {
            element.scrollTop = to;
        }
    };
    requestAnimationFrame(animateScroll);
};

For the utility if you just want to paste this around, here is minimized version of the requestAnimationFrame version:

var scrollTo=function(l,t){var c=document.scrollingElement||document.documentElement,m=c.scrollTop,a=l-m,s=performance.now(),i=function(o){var n,e,r=o-s;c.scrollTop=parseInt((n=r,e=m,o=a,(n/=t/2)<1?o/2*n*n+e:-o/2*(--n*(n-2)-1)+e)),r<t?requestAnimationFrame(i):c.scrollTop=l};requestAnimationFrame(i)};

Then use it like the others

scrollTo(150, 1000);

@Alesvetina

Alesvetina commented Jul 19, 2021

Copy link
Copy Markdown

How would you go about making a linear function, no easing, just scroll down evenly in a set duration? Thank you!

@9mm

9mm commented Aug 27, 2021

Copy link
Copy Markdown

@Alesvetina you would change the easing function for easeInOutQuad = function (t) { ... to simply this:

function (t) { return t; }

Here's 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();
};

@alexsad

alexsad commented Jun 3, 2022

Copy link
Copy Markdown

Hi, an alternative version with top and left props (typescript).

 
const scrollTo = ({element, top, left, duration}: {
        element: HTMLElement, 
        top: number,
        left: number, 
        duration: number,    
    }) => {
        const startTop = element.scrollTop;
        const startLeft = element.scrollLeft;
        const changeTop = top - startTop;
        const changeLeft = left - startLeft;
        const startDate = new Date().getTime();

        const animateScroll = function(){
            const currentDate = new Date().getTime();
            const currentTime = currentDate - startDate;            
            element.scrollTop = easeInOutQuad(currentTime, startTop, changeTop, duration);
            element.scrollLeft = easeInOutQuad(currentTime, startLeft, changeLeft, duration);

            if(currentTime < duration) {
                requestAnimationFrame(animateScroll);
            } else {
                element.scrollTop = top;
                element.scrollLeft = left;
            }
        };
        animateScroll();
    }

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