1

I want to add a class to selected element after scroll ended. How can I detect scroll ended in JS?

HTML

<ul class="list" id="wrapper">
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li id="element">7</li>
    <li>8</li>
    <li>9</li>
    <li>10</li>
 </ul>

JS

const element = document.getElementById('element');
const y = element.getBoundingClientRect().top + window.scrollY;


window.scroll({
  top: y,
  behavior: 'smooth'
});

JSBIN EXAMPLE

midstack
  • 2,105
  • 7
  • 44
  • 73
  • Does this answer your question? [How do I know when I've stopped scrolling?](https://stackoverflow.com/questions/4620906/how-do-i-know-when-ive-stopped-scrolling) – SparkFountain Jan 31 '20 at 08:34
  • Are you checking for scroll process ends or the scroll reaches the bottom of the page? – Ajith Jan 31 '20 at 08:37

5 Answers5

6

2023 Update:

There is now a scrollend event you can listen to. This will fire when the scroll ends, be it from user scroll or from programmatic smooth scroll. The browser support isn't great yet, but it should be available everywhere soon enough.

if (!("onscrollend" in window)) {
  console.warn("Your browser doesn't support the onscrollend event");
}

const trigger = document.getElementById( 'trigger' );
const scroll_forcer = document.getElementById( 'scroll_forcer' );

let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();

function startScroll() {
  setTimeout(()=> {
    scroll_forcer.classList.add( "scrolling" )
    document.scrollingElement.scrollTo( { top: 1000, behavior: "smooth" } );
    document.addEventListener( "scrollend", (evt) => {
      scroll_forcer.classList.remove( "scrolling" );
    }, { once: true });
    }, 10);
};
#scroll_forcer {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
#scroll_forcer.scrolling {
  filter: grayscale(70%);
}
<button id="trigger">click to scroll</button>
<div id="scroll_forcer">
</div>

So for the time being, I may still need the previous answer:


There is natively no event to tell when a smooth-scroll ends.

There is also no standard behavior for how this smooth-scroll should occur, no defined duration, no defined timing function (Chrome uses an ease-in-out function while Firefox uses a linear one), and added to that, a smooth-scroll can be canceled in the middle by an other call to the scroll algorithm, or even have no effect at all...

So this detection is not that easy.

The best way I found for now is to start a requestAnimationFrame powered loop which will check every painting frame (right after the scroll operations), if our target is at the same position. As soon as it has been at the same position for more than two frames, we assume the scrolling ended. That's when we can check if it succeeded, simply by checking if we are at the expected position:

const trigger = document.getElementById( 'trigger' );
const scroll_forcer = document.getElementById( 'scroll_forcer' );

let scrolling = false; // a simple flag letting us know if we're already scrolling
trigger.onclick = (evt) => startScroll();

function startScroll() {
  setTimeout(()=> {
    scroll_forcer.classList.add( "scrolling" )
    smoothScrollTo( { top: 1000 } )
      .catch( (err) => {
        /*
          here you can handle when the smooth-scroll
          gets disabled by an other scrolling
        */
        console.error( 'failed to scroll to target' );
      } )
      // all done, lower the flag
      .then( () => scroll_forcer.classList.remove( "scrolling" ) );
    }, 10);
};


/* 
 *
 * Promised based window.scrollTo( { behavior: 'smooth' } )
 * @param { Element } elem
 **  ::An Element on which we'll call scrollIntoView
 * @param { object } [options]
 **  ::An optional scrollToOptions dictionary
 * @return { Promise } (void)
 **  ::Resolves when the scrolling ends
 *
 */
function smoothScrollTo( options ) {
  return new Promise( (resolve, reject) => {
    const elem = document.scrollingElement;
    let same = 0; // a counter

    // last known scroll positions
    let lastPos_top = elem.scrollTop;
    let lastPos_left = elem.scrollLeft;

    // pass the user defined options along with our default
    const scrollOptions = Object.assign( {
        behavior: 'smooth',
        top: lastPos_top,
        left: lastPos_left
      }, options );

    // expected final position
    const maxScroll_top = elem.scrollHeight - elem.clientHeight;
    const maxScroll_left = elem.scrollWidth - elem.clientWidth;
    const targetPos_top = Math.max( 0, Math.min(  maxScroll_top, scrollOptions.top ) );
    const targetPos_left = Math.max( 0, Math.min( maxScroll_left, scrollOptions.left ) );

    // let's begin
    window.scrollTo( scrollOptions );
    requestAnimationFrame( check );
    
    // this function will be called every painting frame
    // for the duration of the smooth scroll operation
    function check() {
      // check our current position
      const newPos_top = elem.scrollTop;
      const newPos_left = elem.scrollLeft;
      // we add a 1px margin to be safe
      // (can happen with floating values + when reaching one end)
      const at_destination = Math.abs( newPos_top - targetPos_top) <= 1 &&
        Math.abs( newPos_left - targetPos_left ) <= 1;
      // same as previous
      if( newPos_top === lastPos_top &&
        newPos_left === lastPos_left ) {
        if( same ++ > 2 ) { // if it's more than two frames
          if( at_destination ) {
            return resolve();
          }
          return reject();
        }
      }
      else {
        same = 0; // reset our counter
        // remember our current position
        lastPos_top = newPos_top;
        lastPos_left = newPos_left;
      }
      // check again next painting frame
      requestAnimationFrame( check );
    }
  });
}
#scroll_forcer {
  height: 5000vh;
  background-image: linear-gradient(to bottom, red, green);
  background-size: 100% 100px;
}
#scroll_forcer.scrolling {
  filter: grayscale(70%);
}
.as-console-wrapper {
  max-height: calc( 50vh - 30px ) !important;
}
<button id="trigger">click to scroll</button>
<div id="scroll_forcer">
</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
4

You could use requestAnimationFrame to detect when the scrollTop is greater than y

requestAnimationFrame is way better than setting both an interval and an event listener on scroll, for a matter of performance.

const element = document.getElementById('element');
const y = element.getBoundingClientRect().top + window.scrollY;


function checkScrollEnd() {
  if ((window.scrollY || document.body.scrollTop || document.documentElement.scrollTop) < y) {
    window.requestAnimationFrame(checkScrollEnd);
  }
  else {
    alert('end of scroll')   
  }
}

window.requestAnimationFrame(checkScrollEnd);


window.scroll({
  top: y,
  behavior: 'smooth'  
});

Example fiddle

Fabrizio Calderan
  • 120,726
  • 26
  • 164
  • 177
  • 1
    This will start an infinite rAF loop if the scrolling is discarded by an other scroll, or if it didn't produce anything (e.g scrollTo(currentPosition)) – Kaiido Jan 31 '20 at 09:23
  • @Kaiido It Just does what OP asked, It doesn't take into account any context – Fabrizio Calderan Jan 31 '20 at 09:57
  • 1
    It doesn't "detect when scroll ended" no, it does "detect when the scrolltop is bigger than the target position" which says nothing about the scroll action, and will fail in the cases I exposed in my previous comments, that OP never said it's ok to ignore. – Kaiido Jan 31 '20 at 10:19
1

Solution by LINK

const onScrollStop = (callback) => {

        // Make sure a valid callback was provided
        if (!callback || typeof callback !== 'function') return;

        // Setup scrolling variable
        let isScrolling;

        // Listen for scroll events
        window.addEventListener('scroll', (event) => {

            // Clear our timeout throughout the scroll
            window.clearTimeout(isScrolling);

            // Set a timeout to run after scrolling ends
            isScrolling = setTimeout(() => {

                // Run the callback
                callback();

            }, 66);

        }, false);

    };

    onScrollStop(() => {
        console.log('Scrolling has stopped.');
    });
Razi
  • 93
  • 1
  • 10
0

Hope this will help you

<script type="text/javascript">
        jQuery(
          function ($) {
              $(window).on("scroll", function () {
                  var scrollHeight = $(document).height();
                  var scrollPosition = $(window).height() + $(window).scrollTop();
                  if (scrollHeight - scrollPosition <= 180) {
                      // when scroll to bottom of the page

                         // your function to call

                  }
              });
          }
        );
</script>
Joe
  • 40
  • 1
  • 8
0
$(window).scroll(function(){
    if($(window).scrollTop() + $(window).height() >= $(document).height()) {
    //Your Stuff
}
});
Senthil
  • 31
  • 2
  • 1
    Hi and welcome to stackoverflow, and thank you for answering. While this code might answer the question, can you consider adding some explanation for what the problem was you solved, and how you solved it? This will help future readers to understand your answer better and learn from it. – Plutian Jan 31 '20 at 09:31