magritte
magritte

Reputation: 7646

Is there a way to detect horizontal scroll only without triggering a browser reflow

You can detect a browser scroll event on an arbitrary element with:

element.addEventListener('scroll', function (event) {
    // do something
});

I would like to be able to differentiate between vertical scrolling and horizontal scrolling and execute actions for them independently.

I'm currently doing this by stashing the values of element.scrollTop, and element.scrollLeft, and then comparing them inside the event listener. E.g.

var scrollLeft, scrollTop;
element.addEventListener('scroll', function (event) {
    if (scrollLeft !== element.scrollLeft) {
        // horizontally scrolled

        scrollLeft = element.scrollLeft;
    }

    if (scrollTop !== element.scrollTop) {
        // vertically scrolled

        scrollTop = element.scrollTop;
    }
});

This works fine, however from https://gist.github.com/paulirish/5d52fb081b3570c81e3a I read that reading the scrollLeft or scrollTop values causes a reflow.

Is there a better way to do this without causing a browser reflow on scroll?

Upvotes: 30

Views: 45868

Answers (9)

sanghyeon
sanghyeon

Reputation: 1

I gave only the horizontal scroll event in the following way.

const target = document.querySelector('.target');

function eventWheelHorizontal(e) {
  // Horizontal
  if (e.deltaX != '-0') {
    e.preventDefault();
    // working
  }
}

target.addEventListener('wheel', eventWheelHorizontal, { passive: false });

You can share events by applying this simple method.

const target = document.querySelector('.target');

function eventWheel(e) {
  // horizontal
  if (e.deltaX != '-0') {
    e.preventDefault();
    // working
  }
  // Vertical
  if (e.deltaY != '-0') {
    e.preventDefault();
    // working
  }
}

target.addEventListener('wheel', eventWheel, { passive: false });

Upvotes: 0

I've been investing and I come up with this solution:

I had to change a css property to two elements when scrolling horizontally. Hope it helps to anyone anyone who wants to control this aspect. Cheers!

var lastScrollLeft= 0;
$(window).scroll(function(){
    var position= $(document).scrollLeft();
    window.requestAnimationFrame(function() {
    if(position == 0){
            $("#top_box, #helper").css("position","fixed");
    }else if(position !== lastScrollLeft){
            $("#top_box, #helper").css("position","absolute");
            lastScrollLeft= position;
    }
    })
    })

Upvotes: 0

Oleg
Oleg

Reputation: 25008

You can use Intersection Observer API

There's a w3c polyfill available since native support isn't widespread enough.

The polyfill debounces scroll events

Upvotes: 1

Ben West
Ben West

Reputation: 4596

What about listening to the input directly?

element.addEventListener('wheel', e => {
    if ( e.deltaX !== 0 ) {
        horizontal();
    }
    if ( e.deltaY !== 0 ) {
        vertical();
    }
})

You also might need to create your own scrollbars and listen for dragging on them. It's reinventing the wheel (lol) but hey there's no scrollTop or scrollLeft in sight.

Upvotes: 8

sultan
sultan

Reputation: 4739

Try fast-dom library:

Run the example code on your side locally to get correct profiling data.

import fastdom from 'fastdom'

let scrollLeft
let scrollTop
const fast = document.getElementById(`fast-dom`)

fast.addEventListener(`scroll`, function fastDom() {
  fastdom.measure(() => {
    if (scrollLeft !== fast.scrollLeft) {
      scrollLeft = fast.scrollLeft
      console.log(scrollLeft)
    }
    if (scrollTop !== fast.scrollTop) {
      scrollTop = fast.scrollTop
      console.log(scrollTop)
    }
  })
})

enter image description here

Edit fast-dom eaxmple

Upvotes: 1

rodrigocfd
rodrigocfd

Reputation: 8078

As said in @Nelson's answer, there is no way to check between horizontal and vertical scroll, and the reason is because the triggered event object has zero information about it (I just checked it).

Mozilla has a bunch of examples of how to throttle the element property check, but I personally prefer the setTimeout approach, because you can fine-tune the FPS response to match your needs. With that in mind, I wrote this read-to-use example:

<html>
<head>
  <meta charset="utf-8"/>
  <style>
  #foo {
    display: inline-block;
    width: 500px;
    height: 500px;
    overflow: auto;
  }
  #baz {
    display: inline-block;
    background: yellow;
    width: 1000px;
    height: 1000px;
  }
  </style>
</head>
<body>
  <div id="foo">
    <div id="baz"></div>
  </div>
  <script>
    // Using ES5 syntax, with ES6 classes would be easier.
    function WaitedScroll(elem, framesPerSecond, onHorz, onVert) {
      this.isWaiting = false;
      this.prevLeft = 0;
      this.prevTop = 0;
      var _this = this;

      elem.addEventListener('scroll', function() {
        if (!_this.isWaiting) {
          _this.isWaiting = true;
          setTimeout(function() {
            _this.isWaiting = false;
            var curLeft = elem.scrollLeft;
            if (_this.prevLeft !== curLeft) {
              _this.prevLeft = curLeft;
              if (onHorz) onHorz();
            }
            var curTop = elem.scrollTop;
            if (_this.prevTop !== curTop) {
              _this.prevTop = curTop;
              if (onVert) onVert();
            }
          }, 1000 / framesPerSecond);
        }
      });
    }

    // Usage with your callbacks:
    var waiter = new WaitedScroll(
      document.getElementById('foo'), // target object to watch
      15, // frames per second response
      function() { // horizontal scroll callback
        console.log('horz');
      },
      function() { // vertical scroll callback
        console.log('veeeeeeeertical');
      }
    );
  </script>
</body>
</html>

Upvotes: 0

M -
M -

Reputation: 28487

Elements that use position: absolute; are taken out of the document flow (see source).

With this in mind, you could use CSS to make your element absolutely-positioned within its parent...

#parent{
    width: 500px;
    height: 100px; 
    // ...or whatever dimensions you need the scroll area to be
    position: relative;
}
#element{
    position: absolute;
    top: 0px;
    left: 0px;
    bottom: 0px;
    right: 0px;
}

... and then you can feel free to read the element.scrollLeft and element.scrollTop attributes as you did in your original question without fear of bottlenecks from reflowing the entire DOM.

This approach is also recommended in Google's developer guidelines.

Upvotes: 7

I think your code is right, because at the end of the day you need to read one of those properties to find out the scroll direction, but the key thing here to avoid performance problems is to throttle the event, because otherwise the scroll event fires too often and that is the root cause for performance problems.

So your example code adapted to throttle the scroll event would be like this:

var ticking = false;
var lastScrollLeft = 0;
$(window).scroll(function() {
    if (!ticking) {
        window.requestAnimationFrame(function() {

            var documentScrollLeft = $(document).scrollLeft();
            if (lastScrollLeft != documentScrollLeft) {
                console.log('scroll x');
                lastScrollLeft = documentScrollLeft;
            }

            ticking = false;
        });
        ticking = true;
    }
});

Source: https://developer.mozilla.org/en-US/docs/Web/Events/scroll#Example

Upvotes: 13

A. STEFANI
A. STEFANI

Reputation: 6736

I expect that actually no...

But according to How to detect horizontal scrolling in jQuery? you may try:

var lastScrollLeft = 0;
$(window).scroll(function() {
    var documentScrollLeft = $(document).scrollLeft();
    if (lastScrollLeft != documentScrollLeft) {
        console.log('scroll x');
        lastScrollLeft = documentScrollLeft;
    }
});

Need to time-it to double check which method is the quickest one ...

Upvotes: -1

Related Questions