Skip to content

Software Engineer at Polygon

Page scrolling in vanilla JavaScript

I published this article years ago. At the time of writing, this solution was working very well for me. Today I would be using window.scroll and this lightweight smooth scroll behavior polyfill instead.

How many times have you seen the effect of a page scrolling down after clicking a button? Probably thousands! It’s always been extremely easy to do with the popular jQuery library.

$('.js-btn').click(() => {
    $('html, body').animate({
        scrollTop: $('.js-section').offset().top
    }, 200);
});

See the Pen Page scrolling in vanilla JavaScript 1 by Pawel Grzybek (@pawelgrzybek) on CodePen.

It is a decent solution, works great and it’s really well supported across the browsers. But there is a recent trend of abandoning jQuery because pure vanilla JavaScript DOM manipulation is the new hipster skill (I’m one of those hipsters by the way). With the ease of modern APIs and the amount of features that the JavaScript landscape has to offer nowadays it is not that difficult to leave chunky libraries behind.

On one recent project my client asked me to implement this kind of scrolling on his SPA (single page app). Aha! A “challenge” I said! Today I think “DOM-nightmare-inconsistency-mission” is a better term to describe this scenario. If you are one of those hipsters let me save you a couple of hours and share this tiny snippet with you.

Page scrolling without jQuery #

Plan! To start a script it’s always a good idea to have a plan in place. Basically it goes like this:

  1. Determine where to scroll, the duration, the easing function and an optional callback.
  2. On click — grab a timestamp and the current document position.
  3. Scroll to the element as long as you don’t reach the destination.
  4. If the element has finished scrolling trigger an optional callback function.

Determine where to scroll, the duration, the easing function and an optional callback #

All the other steps are always going to be exactly the same. This one may vary depending on the destination, the scrolling duration, the timing function and any callback that is invoked when the scrolling reaches it’s destination. It makes sense to pass all these things as function arguments, right? The destination is the only required argument (ideally it should be a number or DOM element, and function should determine how to deal with it). The duration and easing function possess some sensible default values (thanks to ES2015 default arguments) and the callback function should be optional. Have a look at the wrapper of our function declaration.

function scrollIt(destination, duration = 200, easing = 'linear', callback) {
  // object with some some timing functions
  // function body here
}

On click — grab a timestamp and the current document position #

To calculate values for function that is responsible for scrolling window position up and down, we need to have a reference to initial window value and timestamp.

const start = window.pageYOffset;
const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();

Scroll to the element as long as you don’t reach the destination #

The most popular JavaScript animation solutions are mainly based on setTimeout, setInterval, the WEB Animation API and requestAnimationFrame. The first two are pretty old school. The Web Animation API isn’t made to deal with these kind of situations — read more about it in one of my previous articles. So requestAnimationFrame looks like a perfect candidate for this scenario. We have to be careful tho — it is easy to generate infinite loop if we request a frame loop without providing condition to terminate it. One of those situation can be scrolling below available scrollable window space. Luckily it is not difficult to prevent it. In case that requestAnimationFrame is not available we can just skip animation and move window to the destination. Have a look…

const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
const destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop;
const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);

if ('requestAnimationFrame' in window === false) {
  window.scroll(0, destinationOffsetToScroll);
  if (callback) {
    callback();
  }
  return;
}

function scroll() {
  const now = 'now' in window.performance ? performance.now() : new Date().getTime();
  const time = Math.min(1, ((now - startTime) / duration));
  const timeFunction = easings[easing](time);
  window.scroll(0, Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start));

  requestAnimationFrame(scroll);
}

If the element has finished scrolling trigger an optional callback function #

The last step is to trigger a callback function whenever the document reaches its destination. This requires adding one more line to the condition that checks the current position and destination inside the scroll function.

// Stop requesting animation when window reached its destination
// And run a callback function
if (window.pageYOffset === destinationOffsetToScroll) {
  if (callback) {
    callback();
  }
  return;
}

Putting it all together #

The whole function looks like this.

function scrollIt(destination, duration = 200, easing = 'linear', callback) {

  const easings = {
    linear(t) {
      return t;
    },
    easeInQuad(t) {
      return t * t;
    },
    easeOutQuad(t) {
      return t * (2 - t);
    },
    easeInOutQuad(t) {
      return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },
    easeInCubic(t) {
      return t * t * t;
    },
    easeOutCubic(t) {
      return (--t) * t * t + 1;
    },
    easeInOutCubic(t) {
      return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
    },
    easeInQuart(t) {
      return t * t * t * t;
    },
    easeOutQuart(t) {
      return 1 - (--t) * t * t * t;
    },
    easeInOutQuart(t) {
      return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
    },
    easeInQuint(t) {
      return t * t * t * t * t;
    },
    easeOutQuint(t) {
      return 1 + (--t) * t * t * t * t;
    },
    easeInOutQuint(t) {
      return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
    }
  };

  const start = window.pageYOffset;
  const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();

  const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
  const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
  const destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop;
  const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);

  if ('requestAnimationFrame' in window === false) {
    window.scroll(0, destinationOffsetToScroll);
    if (callback) {
      callback();
    }
    return;
  }

  function scroll() {
    const now = 'now' in window.performance ? performance.now() : new Date().getTime();
    const time = Math.min(1, ((now - startTime) / duration));
    const timeFunction = easings[easing](time);
    window.scroll(0, Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start));

    if (window.pageYOffset === destinationOffsetToScroll) {
      if (callback) {
        callback();
      }
      return;
    }

    requestAnimationFrame(scroll);
  }

  scroll();
}

…and to invoke it

document.querySelector('.js-btn1').addEventListener('click', () => {
  scrollIt(
    document.querySelector('.js-section1'),
    300,
    'easeOutQuad',
    () => console.log(`Just finished scrolling to ${window.pageYOffset}px`)
  );
});

or simply

document.querySelector('.js-btn50000').addEventListener('click', () => scrollIt(50000));

See the Pen PURE JS scrolling by Pawel Grzybek (@pawelgrzybek) on CodePen.

A future solution using scroll-behavior: smooth #

UPDATE! As correctly pointed out by Šime Vidas there is another solution. There is a property of the CSSOM View module called scroll-behavior. This is a native solution for the problem that I’m trying to solve by my script. The implementation is extremely easy, but unfortunately this method isn’t supported well enough to be used reliably (yet). It doesn’t allow us to control timing functions or the duration either. It takes the user-agent values as its defaults. If you want to test examples below, use Firefox or Google Chrome with Experimental Web Platform features flag enabled.

function scrollIt(element) {
  window.scrollTo({
    'behavior': 'smooth',
    'left': 0,
    'top': element.offsetTop
  });
}
const elm = document.querySelector('.js-section');
scrollIt(elm);

See the Pen 2016.07.25 - 3 by Pawel Grzybek (@pawelgrzybek) on CodePen.

And one more example using just a CSS (Firefox only)

body {
  scroll-behavior: smooth;
}
<a href="#one" class="btn">Section 1</a>
<div id="one" class="section">Section 1</div>

See the Pen 2016.07.25 - 4 by Pawel Grzybek (@pawelgrzybek) on CodePen.

Wrap it up #

Please let me know what you think about my solution. I know that the browser support isn’t that amazing compared to the usual jQuery solution. The compromise between browser support, bloating code and performance is a question that you need to answer yourself depending on your project. I had good fun building this script but it’s even more enjoyable for me to share it with you.

Comments

  • Š
    Šime Vidas

    No mention of the standard scrolling API (part of CSSOM View)?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Thank you @simevidas:disqus. I'll update article. Very good reminder. Because of limited browser support I'll be stick to my method for the time being though. Thanks again!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • Š
        Šime Vidas

        There’s a polyfill for the standard API https://github.com/iamdusta... 😊

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
    • M
      Matt Kimont

      I think that till it won't be implemented across major browsers it isn't a standard. History showing that 'standards' from html specs have been removed as some browser didn't implemented that. Other words saying, CSSOM is still in draft, smooth is just a proposition. Also it's 'hard' to call this an API when there is no function imho -> https://drafts.csswg.org/cs...
      which not allowing ex. adding animation.
      I wish that their will implement Pawel approche to CSSOM as this is great code.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • Š
        Šime Vidas

        The smooth option is supported in two browser engines, Chrome and Firefox. The fact that the CSSOM View spec is a draft isn’t relevant, since that’s the standard format in which CSS modules are developed; what matters is how many browser engines support a given feature. In this case, the support from Chrome and Firefox pretty much ensures that the other two engines will implement it eventually, and in the meantime, there’s the polyfill.

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • M
          Matt Kimont

          First flexbox v1 was supported too by major browsers and what's happen with it ? Was scraped.
          Secondly, use polyfiil if they are crucial for your design.
          Thirdly, follow performance vs features :
          - polyfill here is 327 lines of code
          - it's more heavy then code from this article
          - polyfill have differently 'smooth' on safari then in native chrome
          - no option to add different ease on '~native smooth scroll'
          - Chrome release smooth in december last year https://www.chromestatus.co... therefore it's too soon to be sure that there is no bugs on it.

          If you prefer to use that, fair enough.
          I won't use that as same as the other hundreds of polyfills and features till they won't be supported 3 version below by major browsers.

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
          • Š
            Šime Vidas

            You can’t compare this to Flexbox, which was a major feature, an entire new layout model (the first modern layout model in CSS, I think).

            With the standard `smooth` option, you get a feature that already works in the two most-evergreen browser engines and that, btw, works great as progressive enhancement, and whose polyfill is based on a CSS spec. It really can’t get any better than that.

            Yes, the feature is basic, but that’s a good thing. Simpler features are easier to implement, which allows the web platform to move forward in small steps, instead of having a more complex feature scrapped because browsers couldn’t agree on the functionality. Smooth scrolling is valuable even in this most-basic form.

            👆 you can use Markdown here

            Your comment is awaiting moderation. Thanks!
  • S
    Sengokubune

    Pawel this line: (67) const time = Math.min(1, ((now - startTime) / duration)); is GENIOUS and it took me a while to understand it! (Date.now() - startTime) will be >= 1 after the duration! You didn't really elaborate on this part in your blog post so I thought I would ask: Did you came up with this timing solution or you have any source you can link so I can learn more about similar techniques? Thank you in advance for any information and again thank you for all the work you do and share! Cheers from Arizona!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Hi @jakubrpawlowski:disqus.

      Thanks for your kind words. The line that you asked about is a quite common pattern especially when you work with requestAnimationFrame(). I cannot give you any resource because I don't know about any. Keep a simple logic and you will understand it better.

      Essentially requestAnimationFrame() always aims to do something. It aims to move things to particular place, it may do something for some period of time, it may aim to reach some position...

      This one liner checks it requestAnimationFrame() achieved it's aim. If yes — stop. Of not call it again. Simple :)

      Have a great day.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • G
    Gorka Molero

    This is great. Thank you!!!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • R
    Rafał

    Paweł, your solution is exactly what I was looking for. Thank you so much! But I think that I have found a little bug that was appearing in Chrome browser (did not check other browsers). Basically I could not scroll from section 2, 3 or higher back to section 1. It was scrolling up and then jumping back to section that I was before calling scroll. I think it is because Chrome uses float value for document.body.scrollTop. I found solution for this bug and now it works perfectly. I just had to change `if (body.scrollTop === destination) ` into `if (Math.ceil(body.scrollTop) === destination)`
    Pozdrowienia z Polski :)

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Cześć Rafał.

      This blogpost is a bit outdated and I found a much cleaner way to do it in the meantime with exactlly the same browser coverage. Have a look at this codepen...

      http://codepen.io/pawelgrzy...

      I will update this blogpost very soon and implement an updated version of this script here. Maybe even smash some tiny npm module :)

      Pozdrowionka :)

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • P
      Patrick

      You are seeing that probably because `document.querySelector('.js-btn50000')` only returns the *first* element. You can use `querySelectorAll('.className')`, but expect a NodeList back which you then `let...of` through to `addEventListener()`.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • J
    James Towers

    If the destination is less then the windowHeight from the bottom of the page, the script can't reach it and ends up in a never-ending loop trying to get to get to it, to fix this I replaced:

    const  destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop;

    with:

    var destinationOffset = typeof destination === 'number' ? destination : destination.offsetTop;
    if(destinationOffset > (documentHeight - windowHeight)){
    destinationOffset = (documentHeight - windowHeight) - 1;
    }

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • V
    Valentin Damian

    What is the licence of this code?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • J
    Jai Sandhu

    Hey, great tutorial thank you for sharing. One thing the GSAP ScrollTo plugin does well is that it senses if the scroll position was changed outside of itself and cancel that portion of the tween. Any idea how you would go about doing that?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • M
    MobiDevices

    Great work! But there is a problem.

    If you zoom the page in Chrome, the script will try to infinitely scroll. This is due to the fact that window.pageYOffset is not equal to destinationOffsetToScroll.

    Please, fix this. THX!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • E
    Emeka Okoli

    Great post. I love this

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • T
    Taras Yaremkiv

    Hello Pawel. Nice code and description, but one more thing I would add is :
    in case there is a fixed header -

    destinationOffsetToScroll should be replaced with something like
    destinationOffsetToScroll - fixedHeaderOffSet.
    and add default value as 0 for a fixed header height

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • A
    Alexx Bryant

    This is really great and well written, thanks!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Thanks a ton. Happy using :-)

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • J
    Jeremy Marmol

    You save my day.. Thank you very much!!!!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      My pleasure! Hope it helped you out.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • V
    Vladimir Tolstikov

    It's working wrong for me. Correct top position calculation should use getBoundingClientRect as:
    topPos = element.getBoundingClientRect().top + window.scrollY;

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • E
    Eric

    This doesn't seem to be working right for me on Windows. The function kicks off as expected, but then scroll() becomes an infinite loop running over-and-over preventing scrolling on the page as it just bounces me back. I am seeing this same issue both within my project and your codepen.

    It's happening on Edge and Chrome Version 63.0.3239.108 (Official Build) (64-bit). Any guesses?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I came across this issue with this solution at some point. I'll try to dig a solution that I used to fix this issue and I'll refactor a snippet. Thanks fro reporting.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • E
        Eric

        Thanks. It's odd because on my Mac I didn't experience this issue at all so I'm a bit unsure why they would be acting different even when running the same browser. Nonetheless if you do find the fix that'd be great!

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
        • Pawel Grzybek
          Pawel Grzybek

          Hi.
          I think that I found the way to solve an issue. Try to replace this line...


          const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);

          with


          const destinationOffsetToScroll = documentHeight - y < windowHeight ? documentHeight - windowHeight : y;

          Let me know if that helped please.

          👆 you can use Markdown here

          Your comment is awaiting moderation. Thanks!
          • E
            Eric

            That snippet didn't work for me as `y is undefined`. That said, I messed around with this a bit and identified what was happening.

            It seems that if you add an offset to `window.scroll(0, (timeFunction * (destinationOffsetToScroll - start) + start));`, you'll actually get an infinite loop for `time`.

            I forked your codepen to show such an example https://codepen.io/ekfuhrma....

            What ended up resolving the issue was moving the scroll offset out of the following:

            ```
            window.scroll(0, Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start);
            ```

            and into

            ```
            const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);
            ```

            The working line with a variable offset is as follows:

            ```
            const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? (documentHeight - windowHeight) - scrollOffset : destinationOffset - scrollOffset); // scroll offset from top
            ```

            Apologies for taking up some of your time, and thank you for helping me work through it to get a solution.

            EDIT: I'm unsure how to make code snippets here, but hopefully you can parse through it enough to make sense of what I'm trying to say.

            👆 you can use Markdown here

            Your comment is awaiting moderation. Thanks!
          • E
            Eric

            I'm getting `y is Undefined` using that line.

            That said, I poked around a bit deeper and identified the issue and solution.

            I'm not entirely familiar with the time function being used in `scroll()`, but that was the main culprit with my loop.

            I was adding an offset to the scroll to account for some padding I wanted, and was adding it to the following line:


            window.scroll(0, (timeFunction * (destinationOffsetToScroll - start) + start));

            In doing so, it made

            time

            become an infinite loop, breaking the scroll after using it. I threw together an example of this in a fork of your codepen.

            Once I realized the culprit of the infinite loop, I was able to figure out where I could safely add an offset without breaking the whole thing.

            So with that said, if you want to add an offset to the scroll, be sure to do it within the

            destinationOffsetScroll

            variable. My final working variable with the offset looked like the following:


            Math.round(documentHeight - destinationOffset < windowHeight ? (documentHeight - windowHeight) - scrollOffset : destinationOffset - scrollOffset); // scrollOffset is added offset variable

            Again, apologies for the inconvenience and thank you for your time. Though that line didn't ultimately help, it did help me get on the right path to resolving the issue.

            👆 you can use Markdown here

            Your comment is awaiting moderation. Thanks!
            • M
              MK Yoon

              I've encountered same problem - infinite loop - after adding some pixel to scroll or browser zoom-in/out.
              You saved the day! Thank you Eric, and both of you.

              👆 you can use Markdown here

              Your comment is awaiting moderation. Thanks!
            • P
              Patryk Białek

              Please find my hotfix (maybe not perfect but works at least in my project).

              ---
              // The fix to support scrolling up/down on any zoom (Google Chrome)
              // if (window.pageYOffset === destinationOffsetToScroll) {
              const stopResult = (window.pageYOffset / destinationOffsetToScroll).toFixed(2);
              if (stopResult === '1.00' || stopResult === '0.99') {
              if (callback) {
              callback();
              }
              return;
              }
              --

              👆 you can use Markdown here

              Your comment is awaiting moderation. Thanks!
              • k
                kamil

                thanks for sharing

                👆 you can use Markdown here

                Your comment is awaiting moderation. Thanks!
            • H
              Hasan Teoman Tıngır

              Dude I just login bc of upvote that post. Thank you to sharing your solution with us

              👆 you can use Markdown here

              Your comment is awaiting moderation. Thanks!
      • E
        Eric

        Sorry to bug you again regarding this, but was wondering whether or not you found some time to dig up that old solution you had for it?

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • G
    Giannis Savvidis

    Hi.
    First of all, amazing implementation and exactly what I was looking for. A smooth scroll in 100 lines of code instead of having a jquery library as a dependency.
    It works like a charm in linux and mac but on windows it is not, even tho i read all the comments and the possible solutions. First bug is that when I click on the links I cannot scroll from the mouse and the second bug i cant scroll to home(top section), in both there is an infinite loop. Same bugs happen in the code in your codepen. Here is my codepen: https://codepen.io/anon/pen...
    Thanks in advance

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I am aware ot that. It mainly happens when you have a document scaled up or down (via Cmd + or Cmd -). I will find a solution for this oen at some point and come back to this post and update it. I will keep you up to date about my solution progress.

      Thanks

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
      • G
        Giannis Savvidis

        Thank you, I am waitng because i really want to use it :)

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
      • B
        Bartek Dawid Maciejiczek

        Hello Paweł,

        Do you have any update on this?
        I've tried to fix the problem myself but with no success.

        Thanks for great post!

        👆 you can use Markdown here

        Your comment is awaiting moderation. Thanks!
  • A
    Augustin Riedinger

    Nice job.
    For those who want to use it on their website, you can babelify (and eventually minify) it here: https://babeljs.io/repl/

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • K
    Krzysiek-Junior

    Does it correctly work for you in IE 11? I got a bug in IE 11

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • P
      Peter Hraško

      ie and edge, did not work at me

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • S
    Sciampagna Loic

    is there a way to add "current" class when scrolling ?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Hi.

      It is not build in this script, but it is fairly easy to add this functionality. It would be something like this:


      for (const btn of document.querySelectorAll('.btn')) {
      btn.classList.remove('active');
      }

      document.querySelector('.js-btn50000').addEventListener('click', e => {
      scrollIt(50000);
      e.target.classList.add('active');
      });

      Thanks for reading!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • I
    Igor

    nice

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • F
    Flo Knapp

    Thank you for that great piece of code.

    We unfortunately experienced some issues regarding scroll targets with float numbers. In cases where the user has zoomed the page, the target can be determined as a float type, which results in a strange behavior as the site get stuck at the desired scroll target. The user can't scroll any further. Firefox can handle float numbers, but Safari start rounding up that numbers and references it internally still as a float. Thus your condition where the callback is triggered at the desired position is never triggered. Our fix was the following snippet, look for the lastScrollPosition variable. We check if the value in 'lastScrollPosition' hasn't changed and in that case we assume that we reached our desired target:


    var lastScrollPosition;

    function scroll() {

    var now = 'now' in window.performance ? performance.now() : new Date().getTime();
    var time = Math.min(1, ((now - startTime) / duration));
    var timeFunction = easings[easing](time);

    window.scroll(0, Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start));

    /* fix to detect scrolled position to half pixel on browser zoom. */

    if (lastScrollPosition === window.pageYOffset || Math.abs(lastScrollPosition === window.pageYOffsetwindow.pageYOffset - destinationOffsetToScroll) <= 1) {

    if (callback) {
    callback();
    }

    return;

    }

    lastScrollPosition = window.pageYOffset;
    requestAnimationFrame(scroll);

    }


    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Yes. I knew about this issue. As I mentioned on a beginning of this post, I highly suggest using a native scroll API to use, instead of any other custom implementation or library. If you need a browser support, it is then consider solutions like this as a fallback option.

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
    • S
      Simon Jonsson

      if (Math.abs(window.pageYOffset - destinationOffsetToScroll) < 1) {
      is enough, instead of
      if (lastScrollPosition === window.pageYOffset || Math.abs(lastScrollPosition === window.pageYOffsetwindow.pageYOffset - destinationOffsetToScroll) <= 1) {

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • U
    Ulrike Häßler

    Two years later: Really a very fine script, easy to use and working even on my old iPad (iOS9!). Ein herzliches Danke aus Deutschland! Ulrike

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Hi @uhaessler:disqus

      I am glad that you found this article useful. Personally I wouldn't use this approach anymore. Have a look at the note at the very top of this post.

      Have a great day 🥑

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • C
    Caroline Rozali (UnorthodoxThi

    Thanks heaps for the article!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      I am glad it helped you out. Enjoy :)

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • M
    Maciej

    Hi, nice script. I want base on it and make angular directive (NPM). MIT of course :) Can I?

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Sure, but I would highly advise to read an update section on the very top of this article. There are much better techniques to achieve this effect nowadays. Enjoy 🥑

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • c
    cabralada

    After I run this script, only in the Android, I have problem that:
    - I cant scroll the page anymore.

    So suggestion why?


    const scrollItWithEasing = (destination, duration = 200, easing = 'linear', offset = 0, callback) => {

    const easings = {
    linear(t) {
    return t;
    },
    easeInQuad(t) {
    return t * t;
    },
    easeOutQuad(t) {
    return t * (2 - t);
    },
    easeInOutQuad(t) {
    return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
    },
    easeInCubic(t) {
    return t * t * t;
    },
    easeOutCubic(t) {
    return (--t) * t * t + 1;
    },
    easeInOutCubic(t) {
    return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
    },
    easeInQuart(t) {
    return t * t * t * t;
    },
    easeOutQuart(t) {
    return 1 - (--t) * t * t * t;
    },
    easeInOutQuart(t) {
    return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * (--t) * t * t * t;
    },
    easeInQuint(t) {
    return t * t * t * t * t;
    },
    easeOutQuint(t) {
    return 1 + (--t) * t * t * t * t;
    },
    easeInOutQuint(t) {
    return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * (--t) * t * t * t * t;
    }
    };

    const start = window.pageYOffset;
    const startTime = 'now' in window.performance ? performance.now() : new Date().getTime();
    const defaultOffset = offset;

    const documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight);
    const windowHeight = window.innerHeight || document.documentElement.clientHeight || document.getElementsByTagName('body')[0].clientHeight;
    const setDestination = destination.offsetParent.offsetTop === 0 ? destination.offsetTop : destination.offsetParent.offsetTop;
    const destinationOffset = typeof destination === 'number' ? destination : setDestination - defaultOffset;
    const destinationOffsetToScroll = Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset);

    // console.log('documentHeight', documentHeight)
    // console.log('windowHeight', windowHeight)
    // console.log('destinationOffset', destinationOffset)
    // console.log('destinationOffsetToScroll', destinationOffsetToScroll)

    if ('requestAnimationFrame' in window === false) {
    window.scroll(0, destinationOffsetToScroll);
    if (callback) {
    callback();
    }
    return;
    }

    const scroll = () => {
    const now = 'now' in window.performance ? performance.now() : new Date().getTime();
    const time = Math.min(1, ((now - startTime) / duration));
    const timeFunction = easings[easing](time);
    const destination = Math.ceil((timeFunction * (destinationOffsetToScroll - start)) + start);

    window.scroll(0, destination);

    if (window.pageYOffset === destinationOffsetToScroll) {

    if (callback) {
    callback();
    }
    return;
    }

    requestAnimationFrame(scroll);
    }

    scroll();
    }

    export default scrollItWithEasing;

    And I call it via another function with:

    scrollItWithEasing(
    document.getElementById(findId),
    300,
    'easeInOutQuad',
    setOffset
    );

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • G
      Gordon Freeman

      Hello, yes probaly it is an infinite loop because the resulting scrollOffset did not 100% match the expected destinationOffset.

      At best have a look at the other post where I have introduced improvements to not have that problem ;-)

      Cheers!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • G
    Gordon Freeman

    Hello, And thank you for this article :D

    Helped me alot to build my solution for this scenario - therefore I made some improvements I'd like to share


    function getScrollOffset() {
    var destinationOffset = typeof destination == 'number' ? destination : destination.offsetTop
    var documentHeight = Math.max(document.body.scrollHeight, document.body.offsetHeight, document.documentElement.clientHeight, document.documentElement.scrollHeight, document.documentElement.offsetHeight)
    var windowHeight = window.innerHeight or document.documentElement.clientHeight or document.getElementsByTagName('body')[0].clientHeight
    return Math.round(documentHeight - destinationOffset < windowHeight ? documentHeight - windowHeight : destinationOffset)
    }

    function scroll() {
    var scrollOffset = getScrollOffset() /* <- recalculate the scroll offset */

    var now = 'now' of window.performance ? performance.now() : (new Date).getTime()
    var time = Math.min(1, (now - startTime) / duration)
    var timeFunction = easings[easing](time)
    window.scroll 0, Math.ceil(timeFunction * (scrollOffset - start) + start)

    if(now >= (startTime + duration)) { /* <- terminate for sure after time is over */
    if(callback)
    callback()
    return
    }
    requestAnimationFrame(scroll)
    return
    }

    So basicly I calculate the desired destinationScrollOffset for each animationFrame.
    Because sometimes I have geometry changes like collapsing menues onScroll ;-)

    Next is to make the termination of the scrolling dependent on the time instead of the scrollOffset.
    This gets rid of the possible infite loops.

    IMO the "inaccuracy" is ok in any way because it cannot reliably be made 100% accurate. (That's why the infinite loops happen at some point.)

    Cheers!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
  • V
    Vyacheslav Panchuk

    document.getElementById("scroll_to_results").scrollIntoView({
    block: 'start',
    behavior: 'smooth'
    });

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Yes! This is exactly what I suggest using nowadays.

      https://pawelgrzybek.com/pa...

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • P
    Paweł Mysior

    Great post, thanks!

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!
    • Pawel Grzybek
      Pawel Grzybek

      Pleasure. Have a fab weekend!

      👆 you can use Markdown here

      Your comment is awaiting moderation. Thanks!
  • B
    Bipul chandra Nath

    Nice work

    👆 you can use Markdown here

    Your comment is awaiting moderation. Thanks!

Leave a comment

👆 you can use Markdown here

Your comment is awaiting moderation. Thanks!