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

9

28

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?

Hypochondriasis answered 8/2, 2017 at 11:42 Comment(2)
Track your element's scrollLeft and scrollTop values, and see if they changed when you handle a scroll event, updating them at the end of the handling so they're ready for the next event handling?Bright
If you didn't change the DOM structure between your events, it won't trigger a reflow, or more exactly, the reflow will have nothing to do. Nelson is right, you should anyway throttle scroll events, but it's not because of the reflow, but simply because it makes little sense to react faster than screen refresh rate to this "graphical" event.Abdulabdulla
K
12

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

Km answered 22/3, 2018 at 19:7 Comment(1)
Yes it's good to use rAF to throttle this event, might even be better to use passive events in case you don't want to preventDefault the event.Abdulabdulla
T
7

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.

Tawana answered 26/3, 2018 at 22:47 Comment(1)
very good answer thank you... this simple trick can improve the performance by so much when finding out the top/left property using getBoundingClientRect for stuff like parallax scrollingCarbamate
W
6

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.

Wendeline answered 28/3, 2018 at 11:27 Comment(3)
This should be the accepted answer for mouse-wheel only. Dragging scrolling bar horizontally does not reach this code as the DOM emitted event is different.Hesse
This is perfect answer if we just replace 'wheel' with 'scroll' because for wheel it won't listen for touch scroll and scrollbar dragging by mouseGoggle
There’s no deltaX or deltaY on a scroll eventWendeline
K
1

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

Kirkkirkcaldy answered 26/3, 2018 at 17:54 Comment(0)
D
1

You can use Intersection Observer API

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

The polyfill debounces scroll events

Discriminatory answered 28/3, 2018 at 20:45 Comment(0)
S
0

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>
Sixty answered 27/3, 2018 at 1:33 Comment(0)
A
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;
    }
    })
    })
Ashtonashtonunderlyne answered 21/6, 2019 at 7:13 Comment(0)
H
0

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 });
Harber answered 7/4, 2022 at 4:23 Comment(0)
N
-1

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 ...

Neibart answered 22/3, 2018 at 18:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.