How does debouncing scroll events in JS/JQuery work?

Viewed 1068

The scroll events trigger way more often than I want them to. All it should do is give the .swap class to the next Sibling if you scroll down (followed by an animation) and get rid of the .swap class of the current Element. Same thing just for upwards. What happens now is that it just basically goes to the last element and the first one if scrolled down/up. It is a one-pager which just switches in between different divs.

I've tried Debouncing, Throttling, and requestAnimationFrame but haven't been successful so far. For example: https://css-tricks.com/debouncing-throttling-explained-examples/

Thanks a lot!

HTML:

<div class="div1 swap">
  <h1>Hello</h1>
</div>
<div class="div2">
  <h1>Stack</h1>
</div>
<div class="div3">
  <h1>Overflow</h1>
</div>
<div class="div4">
  <h1>Please</h1>
</div>
<div class="div5">
  <h1>Help</h1>
</div>

CSS:

.div1, .div2, .div3, .div4, .div5{
  visibility: hidden;
  position: fixed;
}
.swap{
  visibility: visible;
  -webkit-animation: slide-in-left .8s ease-out both;
          animation: slide-in-left .8s ease-out both;
}

JS:

window.onscroll = function(event) {
  var aktiv = document.querySelector('.swap');
  if(this.oldScroll > this.scrollY){
    console.log("Up");
    aktiv.previousElementSibling.classList.toggle('swap')
    aktiv.classList.toggle('swap')
  }
  else{
    console.log("Down");
    aktiv.nextElementSibling.classList.toggle('swap')
    aktiv.classList.toggle('swap')
  }
  this.oldScroll = this.scrollY;
}
2 Answers

Add some threshold for scroll distance, as now even if you scroll 1px. you show next element.

window.onscroll = function(event) {
  var aktiv = document.querySelector('.swap');
  var diff = this.oldScroll - this.scrollY;
  var delta = 100; // Or some element height

  // If scolled too little - do nothing
  if (Match.abs(diff) <= delta) {
     return;
  }

  if(diff < 0){
    console.log("Up");
    aktiv.previousElementSibling.classList.toggle('swap')
    aktiv.classList.toggle('swap')
  }
  else{
    console.log("Down");
    aktiv.nextElementSibling.classList.toggle('swap')
    aktiv.classList.toggle('swap')
  }
  this.oldScroll = this.scrollY;
}

To debounce event use setTimeout:

var timer;

window.onscroll = function (event) {
    var self = this;
    clearTimeout(timer);
    timer = setTimeout(function () {
        [...rest of code...]
    }, 50)
}

You should not use scroll event listener for this, this is best done with Intersection Observer (IO).

If you want to trigger animations on more elements than one, watching these multiple elements via scroll event listener can result in bad performance. See for example this question. disclaimer: This question also has my answer.

To solve this in a modern, performant way it's best to use Intersection Observer (IO) for this.

With IO you can watch one (or multiple) elements and react once they come into view or if they intersect each other.

To use IO you first have to first set the options for it, then define which element(s) to watch and last to define what exactly happens once the IO triggers.

Example (Taken from here), with one minimal modification: The author removed the IO even if the animation didn't happen yet. I moved the unobserve call inside the check if element is visible.

const SELECTOR = '.watched';
const ANIMATE_CLASS_NAME = 'animated';

const animate = element => (
  element.classList.add(ANIMATE_CLASS_NAME)
);

const isAnimated = element => (
  element.classList.contains(ANIMATE_CLASS_NAME)
);

const intersectionObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach((entry) => {
    
    // when element's is in viewport,
    // animate it!
    if (entry.intersectionRatio > 0) {
      animate(entry.target);
      // remove observer after animation
      observer.unobserve(entry.target);
    }
  });
});

// get only these elements,
// which are not animated yet
const elements = [].filter.call(
  document.querySelectorAll(SELECTOR),
  element => !isAnimated(element, ANIMATE_CLASS_NAME)
);
//console.log(elements);

// start observing your elements
elements.forEach((element) => intersectionObserver.observe(element));
.h100 {
height: 100vh;
}

.watched {
 opacity: 0;
 transition: opacity .5s;
}

.watched.animated {
opacity: 1;
}
<div class="watched hidden">
I'm watched and I'm in the viewport on page load!
</div>
<div class="h100">
scroll down
</div>
<div class="watched">
I'm watched
</div>
<div class="h100">
Another element, keep scrolling
</div>
<div class="watched">
I'm also watched
</div>

Related