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;
};
@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