Skip to content

Instantly share code, notes, and snippets.

@joshbeckman
Created September 30, 2013 14:51
Show Gist options
  • Save joshbeckman/6764939 to your computer and use it in GitHub Desktop.
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

Thank you very much! 😄

@Cdvalencia
Copy link

Cdvalencia commented Aug 22, 2018

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
Copy link

hugotox commented Sep 5, 2018

Just curious, why mutate the global Math object?

@tomaswolfgang
Copy link

tomaswolfgang commented Sep 6, 2018

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

Thanks so much, great solution! :)

@c-emil
Copy link

c-emil commented Sep 18, 2018

@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

@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

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

Thanks!!

@felipenmoura
Copy link

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


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
Copy link

ruucm commented May 6, 2019

Thanks a lot!

@marsderp
Copy link

marsderp commented May 9, 2019

Has anyone added more easing functions?

@orrybaram
Copy link

orrybaram commented Jun 18, 2019

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
Copy link

wnsgh78b commented Aug 21, 2019

	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
Copy link

darkhorse-coder commented Nov 20, 2019

@andjosh Thank you!! Very useful!!

@joloiuhj
Copy link

@friedboats
Copy link

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

@mohsenasm
Copy link

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

@emanavas
Copy link

Works Great. Thanks!!

@RaymondBakker
Copy link

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

Nice resource, is very useful 🥇

@KarloZKvasin
Copy link

KarloZKvasin commented Nov 4, 2020

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
Copy link

dhovart commented Dec 17, 2020

@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

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

@ruucm
Copy link

ruucm commented Feb 17, 2021

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
Copy link

Ciantic commented Mar 17, 2021

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
Copy link

Alesvetina commented Jul 19, 2021

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

@9mm
Copy link

9mm commented Aug 27, 2021

@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
Copy link

alexsad commented Jun 3, 2022

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