Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I detect window scroll ended in javascript?

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

like image 457
midstack Avatar asked Aug 06 '26 07:08

midstack


2 Answers

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>
like image 168
Kaiido Avatar answered Aug 08 '26 21:08

Kaiido


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

like image 36
Fabrizio Calderan Avatar answered Aug 08 '26 20:08

Fabrizio Calderan



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!