82

How do I know when I've stopped scrolling using Javascript?

akinuri
  • 10,690
  • 10
  • 65
  • 102
krishna
  • 845
  • 2
  • 8
  • 5
  • I've just written a small script to accomplish this. Syntax is similar to `.addEventListener()`. Can be found at [github.com/akinuri/scroll](https://github.com/akinuri/scroll). Here's a [demo](https://akinuri.github.io/scroll). – akinuri Apr 29 '19 at 12:15
  • **There is a `scrollend` event now...** although as of 2022 it doesn't quite have full support yet. see [Documentation & Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Document/scrollend_event) for info. – ashleedawg Jan 10 '23 at 10:19

9 Answers9

131

You can add an event handler for the scroll event and start a timeout. Something like:

var timer = null;
window.addEventListener('scroll', function() {
    if(timer !== null) {
        clearTimeout(timer);        
    }
    timer = setTimeout(function() {
          // do something
    }, 150);
}, false);

This will start a timeout and wait 150ms. If a new scroll event occurred in the meantime, the timer is aborted and a new one is created. If not, the function will be executed. You probably have to adjust the timing.

Also note that IE uses a different way to attach event listeners, this should give a good introduction: quirksmode - Advanced event registration models

Felix Kling
  • 795,719
  • 175
  • 1,089
  • 1,143
  • I would suggest callback – Mo. Dec 04 '13 at 15:30
  • @bboymaanu: I don't know what you are trying to tell me with that comment. Please elaborate. – Felix Kling Dec 04 '13 at 15:51
  • Thanks for your comment, The issue is that when hold scroll still `setTimeout` working :(, is there any solution that when stopped working then run the code, Like `jQuery` callback ?# – Mo. Dec 04 '13 at 16:48
  • 3
    I still don't understand the problem. A callback is just a function which is passed to and called by another function. E.g. the function passed to `setTimeout` is a callback or the the event handler is a callback. Anyways, if you have an actual problem/question, please [ask a question](http://stackoverflow.com/questions/ask). – Felix Kling Dec 04 '13 at 16:51
  • Perfect! Worked for me. I'm was using Safari 8.0.3 when I ran the code. – www139 Apr 05 '15 at 01:30
  • 3
    if `$(window)` is jQuery, `addEventListener` is undefined. `$(window).on('scroll')` might work. – commonpike Nov 27 '16 at 11:54
  • why 150ms , just set a long enough time ? – hirra Jul 03 '19 at 03:19
  • Old answer, but I found this when looking so is still relevant. The addEventListener funciton should have True as it's last parameter for this to fire on the scroll event. – Kyohei Kaneko Dec 17 '19 at 10:29
  • This answer is almost great, but why on earth do `$(window)` when you're just calling a standard DOM function? – jbg Dec 29 '19 at 10:26
50

There isn't a "Stopped Scrolling" event. If you want to do something after the user has finished scrolling, you can set a timer in the "OnScroll" event. If you get another "OnScroll" event fired then reset the timer. When the timer finally does fire, then you can assume the scrolling has stopped. I would think 500 milliseconds would be a good duration to start with.

Here's some sample code that works in IE and Chrome:

<html>

<body onscroll="bodyScroll();">

  <script language="javascript">
    var scrollTimer = -1;

    function bodyScroll() {
      document.body.style.backgroundColor = "white";

      if (scrollTimer != -1)
        clearTimeout(scrollTimer);

      scrollTimer = window.setTimeout("scrollFinished()", 500);
    }

    function scrollFinished() {
      document.body.style.backgroundColor = "red";
    }
  </script>

  <div style="height:2000px;">
    Scroll the page down. The page will turn red when the scrolling has finished.
  </div>

</body>

</html>
Lee Taylor
  • 7,761
  • 16
  • 33
  • 49
David
  • 34,223
  • 3
  • 62
  • 80
  • 2
    Thanks for the pure javascript, NON-jQuery code, and the effective example! It works well on Firefox, too. – Panini Luncher Nov 13 '15 at 16:23
  • Pure JS is hard to find on stack these days, thanks for the legit non-jQuery solution. – metaColin Mar 09 '16 at 09:43
  • 1
    @MerakMarey I guess, you're not familiar with [Needs More jQuery](https://needsmorejquery.com/). Sometimes pure JavaScript is a rare treat :) _P.S. I'm, for one, not interested in the magic show. I want to know what's happening behind the curtain._ – akinuri Apr 29 '19 at 12:31
  • 1
    @akinuri That is a one good joke, but that's all it is...a joke...The idea of frameworks abstracting all the "magic work" is more beneficial than knowing what's behind the curtain..(which you can still do, if you want to...jQuery IS still Javascript..you can change, and even improve the code or whatev you want to do..)..but this will put you off the main goal, which is to get the job done IN TIME..which is MORE IMPORTANT than knowing all the little details...believe me, I one time I had the same opinion..but no...now I do : `var a =1; var b =null; $.when(b=a+2).then(alert(b)); ` :) – Merak Marey Apr 30 '19 at 23:00
  • **Actually there is a `scrollend` event** now... although as of 2022 it doesn't quite have full support yet. see [Documentation & Compatibility](https://developer.mozilla.org/en-US/docs/Web/API/Document/scrollend_event) – ashleedawg Jan 10 '23 at 10:18
17

Here's a more modern, Promise-based solution I found on a repo called scroll-into-view-if-needed

Instead of using addEventListener on the scroll event it uses requestAnimationFrame to watch for frames with no movement and resolves when there have been 20 frames without movement.

function waitForScrollEnd () {
    let last_changed_frame = 0
    let last_x = window.scrollX
    let last_y = window.scrollY

    return new Promise( resolve => {
        function tick(frames) {
            // We requestAnimationFrame either for 500 frames or until 20 frames with
            // no change have been observed.
            if (frames >= 500 || frames - last_changed_frame > 20) {
                resolve()
            } else {
                if (window.scrollX != last_x || window.scrollY != last_y) {
                    last_changed_frame = frames
                    last_x = window.scrollX
                    last_y = window.scrollY
                }
                requestAnimationFrame(tick.bind(null, frames + 1))
            }
        }
        tick(0)
    })
}

With async/await and then

await waitForScrollEnd()

waitForScrollEnd().then(() => { /* Do things */ })

Additionally, there's the scrollend event which will eventually be the definitive answer, but as of June 2023 seems to only be fully supported on Chrome and Firefox.

document.onscrollend = event => {
  // scroll ended
};

aScrollingElement.onscrollend = (event) => {
  // scroll ended
};

window.addEventListener("scrollend", (event) => {
  // scroll ended
});

aScrollingElement.addEventListener("scrollend", (event) => {
  // scroll ended
});

scrollend on MDN
scrollend Chrome Blog

Sam Carlton
  • 1,190
  • 1
  • 17
  • 18
5
(function( $ ) {
        $(function() {
            var $output = $( "#output" ),
                scrolling = "<span id='scrolling'>Scrolling</span>",
                stopped = "<span id='stopped'>Stopped</span>";
                $( window ).scroll(function() {
                    $output.html( scrolling );
                    clearTimeout( $.data( this, "scrollCheck" ) );
                    $.data( this, "scrollCheck", setTimeout(function() {
                        $output.html( stopped );
                    }, 250) );
    
                });
        });
    })( jQuery );

=======>>>> Working Example here

Community
  • 1
  • 1
Muhammad Tahir
  • 2,351
  • 29
  • 25
  • 1
    Please don't post identical answers to multiple questions. Post one good answer, then vote/flag to close the other questions as duplicates. If the question is not a duplicate, _tailor your answers to the question_ – kleopatra Aug 21 '15 at 07:45
  • @kleopatra Good suggestion. the reason i posted this answer to every question so that everyone can get benefit from it. there are so many questions related to this. maybe it could be useful for anyone on any question. – Muhammad Tahir Aug 21 '15 at 11:11
  • it worked when all the other methods failed. However, could you please explain the "scrollCheck" in a bit detail? – Vipin Verma Oct 15 '15 at 12:26
3

I did something like this:

var scrollEvents = (function(document, $){

    var d = {
        scrolling: false,
        scrollDirection : 'none',
        scrollTop: 0,
        eventRegister: {
            scroll: [],
            scrollToTop: [],
            scrollToBottom: [],
            scrollStarted: [],
            scrollStopped: [],
            scrollToTopStarted: [],
            scrollToBottomStarted: []
        },
        getScrollTop: function(){ 
            return d.scrollTop;
        },
        setScrollTop: function(y){
            d.scrollTop = y;
        },
        isScrolling: function(){
            return d.scrolling;
        },
        setScrolling: function(bool){
            var oldVal = d.isScrolling();
            d.scrolling = bool;
            if(bool){
                d.executeCallbacks('scroll');
                if(oldVal !== bool){
                    d.executeCallbacks('scrollStarted');
                }
            }else{
                d.executeCallbacks('scrollStopped');
            }
        },
        getScrollDirection : function(){
            return d.scrollDirection;
        },
        setScrollDirection : function(direction){
            var oldDirection = d.getScrollDirection();
            d.scrollDirection = direction;
            if(direction === 'UP'){
                d.executeCallbacks('scrollToTop');
                if(direction !== oldDirection){
                    d.executeCallbacks('scrollToTopStarted');
                }
            }else if(direction === 'DOWN'){
                d.executeCallbacks('scrollToBottom');
                if(direction !== oldDirection){
                    d.executeCallbacks('scrollToBottomStarted');
                }
            }
        },
        init : function(){
            d.setScrollTop($(document).scrollTop());
            var timer = null;
            $(window).scroll(function(){
                d.setScrolling(true);
                var x = d.getScrollTop();
                setTimeout(function(){
                    var y = $(document).scrollTop();
                    d.setScrollTop(y);
                    if(x > y){
                        d.setScrollDirection('UP');
                    }else{
                        d.setScrollDirection('DOWN');
                    }
                }, 100);
                if(timer !== 'undefined' && timer !== null){
                    clearTimeout(timer);
                }
                timer = setTimeout(function(){
                    d.setScrolling(false);
                    d.setScrollDirection('NONE');
                }, 200);
            });
        },
        registerEvents : function(eventName, callback){
            if(typeof eventName !== 'undefined' && typeof callback === 'function' && typeof d.eventRegister[eventName] !== 'undefined'){
                d.eventRegister[eventName].push(callback);
            }
        },
        executeCallbacks: function(eventName){
            var callabacks = d.eventRegister[eventName];
            for(var k in callabacks){
                if(callabacks.hasOwnProperty(k)){
                    callabacks[k](d.getScrollTop());
                }
            }
        }
    };
    return d;

})(document, $);

the code is available here: documentScrollEvents

kaiser
  • 21,817
  • 17
  • 90
  • 110
sri_wb
  • 339
  • 1
  • 6
  • 16
2

I was trying too add a display:block property for social icons that was previously hidden on scroll event and then again hide after 2seconds. But

I too had a same problem as my code for timeout after first scroll would start automatically and did not had reset timeout idea. As it didn't had proper reset function.But after I saw David's idea on this question I was able to reset timeout even if someone again scrolled before actually completing previous timeout.

  1. problem code shown below before solving

    $(window).scroll(function(){
      setTimeout(function(){
         $('.fixed-class').slideUp('slow');
       },2000);
    });
    

  1. edited and working code with reset timer if next scroll occurs before 2s

    var timer=null;
    $(window).scroll(function(){
       $('.fixed-class').css("display", "block");
       if(timer !== null) {
          clearTimeout(timer);
    } timer=setTimeout(function(){ $('.fixed-class').slideUp('slow'); },2000);

    });

My working code will trigger a hidden division of class named 'fixed-class' to show in block on every scroll. From start of latest scroll the timer will count 2 sec and then again change the display from block to hidden.

Ahsit Tamang
  • 78
  • 1
  • 10
1

Minor update in your answer. Use mouseover and out function.

 $(document).ready(function() {
           function ticker() {
    $('#ticker li:first').slideUp(function() {
        $(this).appendTo($('#ticker')).slideDown();
    });
}

var ticke= setInterval(function(){ 
                            ticker(); 
            }, 3000);
       $('#ticker li').mouseover(function() { 
          clearInterval(ticke);
      }).mouseout(function() { 
          ticke= setInterval(function(){ ticker(); }, 3000);
        });

        });

DEMO

Ivin Raj
  • 3,448
  • 2
  • 28
  • 65
1

For more precision you can also check the scroll position:

function onScrollEndOnce(callback, target = null) {
    let timeout
    let targetTop
    const startPosition = Math.ceil(document.documentElement.scrollTop)

    if (target) {
        targetTop = Math.ceil(target.getBoundingClientRect().top + document.documentElement.scrollTop)
    }

    function finish(removeEventListener = true) {
        if (removeEventListener) {
            window.removeEventListener('scroll', onScroll)
        }

        callback()
    }

    function isScrollReached() {
        const currentPosition = Math.ceil(document.documentElement.scrollTop)

        if (targetTop == null) {
            return false
        } else if (targetTop >= startPosition) {
            return currentPosition >= targetTop
        } else {
            return currentPosition <= targetTop
        }
    }

    function onScroll() {
        if (timeout) {
            clearTimeout(timeout)
        }

        if (isScrollReached()) {
            finish()
        } else {
            timeout = setTimeout(finish, 500)
        }
    }

    if (isScrollReached()) {
        finish(false)
    } else {
        window.addEventListener('scroll', onScroll)
    }
}

Usage example:

const target = document.querySelector('#some-element')

onScrollEndOnce(() => console.log('scroll end'), target)

window.scrollTo({
    top: Math.ceil(target.getBoundingClientRect().top + document.documentElement.scrollTop),
    behavior: 'smooth',
})
Mihail H.
  • 1,555
  • 16
  • 18
0

Here's an answer that doesn't use any sort of timer, thus in my case predicted when the scrolling actually ended, and is not just paused for a bit.

function detectScrollEnd(element, onEndHandler) {
    let scrolling = false;
    
    element.addEventListener('mouseup', detect);
    element.addEventListener('scroll', detect);

    function detect(e) {
        if (e.type === 'scroll') {
            scrolling = true;
        } else {
            if (scrolling) {
                scrolling = false;
                onEndHandler?.();
            }
        }
    }
}
DarkMental
  • 482
  • 7
  • 26