77

So, how do I know the scroll direction when the event it's triggered?

In the returned object the closest possibility I see is interacting with the boundingClientRect kind of saving the last scroll position but I don't know if handling boundingClientRect will end up on performance issues.

Is it possible to use the intersection event to figure out the scroll direction (up / down)?

I have added this basic snippet, so if someone can help me.
I will be very thankful.

Here is the snippet:

var options = {
  rootMargin: '0px',
  threshold: 1.0
}

function callback(entries, observer) { 
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      console.log('entry', entry);
    }
  });
};

var elementToObserve = document.querySelector('#element');
var observer = new IntersectionObserver(callback, options);

observer.observe(elementToObserve);
#element {
  margin: 1500px auto;
  width: 150px;
  height: 150px;
  background: #ccc;
  color: white;
  font-family: sans-serif;
  font-weight: 100;
  font-size: 25px;
  text-align: center;
  line-height: 150px;
}
<div id="element">Observed</div>

I would like to know this, so I can apply this on fixed headers menu to show/hide it

1
  • 6
    "but I don't [know?] if handling boundingClientRect will end up on performance issues" - you get the boundingClientRect passed inside the IntersectionObserverEntry already, whether you're asking for it or not. Any "performance penalty" calculating the bounding client rectangle entails has already occurred, nothing you could do about that. So you might as well make use of the top or bottom offset that was calculated already and compare it to the stored previous value, instead of letting the effort that's already been made go to waste ...
    – CBroe
    Commented Nov 5, 2017 at 2:49

6 Answers 6

69

I don't know if handling boundingClientRect will end up on performance issues.

MDN states that the IntersectionObserver does not run on the main thread:

This way, sites no longer need to do anything on the main thread to watch for this kind of element intersection, and the browser is free to optimize the management of intersections as it sees fit.

MDN, "Intersection Observer API"

We can compute the scrolling direction by saving the value of IntersectionObserverEntry.boundingClientRect.y and compare that to the previous value.

Run the following snippet for an example:

const state = document.querySelector('.observer__state')
const target = document.querySelector('.observer__target')

const thresholdArray = steps => Array(steps + 1)
 .fill(0)
 .map((_, index) => index / steps || 0)

let previousY = 0
let previousRatio = 0

const handleIntersect = entries => {
  entries.forEach(entry => {
    const currentY = entry.boundingClientRect.y
    const currentRatio = entry.intersectionRatio
    const isIntersecting = entry.isIntersecting

    // Scrolling down/up
    if (currentY < previousY) {
      if (currentRatio > previousRatio && isIntersecting) {
        state.textContent ="Scrolling down enter"
      } else {
        state.textContent ="Scrolling down leave"
      }
    } else if (currentY > previousY && isIntersecting) {
      if (currentRatio < previousRatio) {
        state.textContent ="Scrolling up leave"
      } else {
        state.textContent ="Scrolling up enter"
      }
    }

    previousY = currentY
    previousRatio = currentRatio
  })
}

const observer = new IntersectionObserver(handleIntersect, {
  threshold: thresholdArray(20),
})

observer.observe(target)
html,
body {
  margin: 0;
}

.observer__target {
  position: relative;
  width: 100%;
  height: 350px;
  margin: 1500px 0;
  background: rebeccapurple;
}

.observer__state {
  position: fixed;
  top: 1em;
  left: 1em;
  color: #111;
  font: 400 1.125em/1.5 sans-serif;
  background: #fff;
}
<div class="observer__target"></div>
<span class="observer__state"></span>


If the thresholdArray helper function might confuse you, it builds an array ranging from 0.0 to 1.0by the given amount of steps. Passing 5 will return [0.0, 0.2, 0.4, 0.6, 0.8, 1.0].

7
  • 1
    awesome, can you explain your snippet here please: const thresholdArray = steps => Array(steps + 1) .fill(0) .map((_, index) => index / steps || 0)
    – DiaJos
    Commented Feb 15, 2019 at 14:13
  • 6
    This just creates a new array with 20 empty slots and then fills each slot with zero and then uses map() to divide the index by 20. This results in 0.05,0.1,0.15,0.2, and so on until you reach index 20, which equates to 1.0 [20/20]. These are Intersection threshold points. Commented Apr 7, 2019 at 23:22
  • 3
    Your conditions are not set correctly on your if/else statements. You need to have the isIntersecting variable as a condition added to the "Scrolling up leave" if statement and you need to add the ! logical operator to it. Like so: if (currentRatio < previousRatio && !isIntersecting). You have your isIntersecting variable in your first else if statement, which breaks functionality when you try scrolling up/leaving. That variable does not need to be in the higher condition. Not sure if that is a mistake or negligence.
    – Xavier
    Commented Nov 17, 2019 at 19:43
  • It has been some time, but from what I understood isIntersecting returns a bool as to whether the target is in view or not. Applying if (currentRatio < previousRatio && !isIntersecting) would result in "Scrolling up leave" only being called once the entire target has left the view? If so, this would not allow for effects to be applied over time as the target leaves the view.
    – Jason
    Commented Nov 18, 2019 at 10:21
  • I think using boundingClientRect together with the IntersectionObserver is not a good idea, because the whole point of the IntersectionObserver was that you don't need boundingClientRect any more. If you need to know the scroll direction you'll better use the not so costly property like window.pageYOffset.
    – bitWorking
    Commented May 9, 2020 at 11:57
28

This solution is without the usage of any external state, hence simpler than solutions which keep track of additional variables:

const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.boundingClientRect.top < 0) {
          if (entry.isIntersecting) {
            // entered viewport at the top edge, hence scroll direction is up
          } else {
            // left viewport at the top edge, hence scroll direction is down
          }
        }
      },
      {
        root: rootElement,
      },
    );
2
  • 1
    Very nice solution thank you very much! In my case I was struggling a bit because I had several small objects to observe. Therefor I have combined the middle part of your example and it worked well then: if (entry.isIntersecting && entry.boundingClientRect.top < 0) { } else if (entry.isIntersecting && entry.boundingClientRect.top > 0) {}
    – Cenco
    Commented Feb 20, 2023 at 12:57
  • this breaks if the boundingClientRect is greater than the viewport. my solution wouldnt be affected by this issue
    – oldboy
    Commented Jul 12, 2023 at 18:46
15

Comparing boundingClientRect and rootBounds from entry, you can easily know if the target is above or below the viewport.

During callback(), you check isAbove/isBelow then, at the end, you store it into wasAbove/wasBelow. Next time, if the target comes in viewport (for example), you can check if it was above or below. So you know if it comes from top or bottom.

You can try something like this:

var wasAbove = false;

function callback(entries, observer) {
    entries.forEach(entry => {
        const isAbove = entry.boundingClientRect.y < entry.rootBounds.y;

        if (entry.isIntersecting) {
            if (wasAbove) {
                // Comes from top
            }
        }

        wasAbove = isAbove;
    });
}

Hope this helps.

1
  • 2
    This can fail to work in some rare cases, e.g. when scrolling without animation from a scroll position where the target is below the viewport directly to a scroll position above the viewport. Since the target is not intersecting in both cases, the browser does not trigger a callback.
    – fabb
    Commented Mar 13, 2020 at 21:15
2

My requirement was:

  • do nothing on scroll-up
  • on scroll-down, decide if an element started to hide from screen top

I needed to see a few information provided from IntersectionObserverEntry:

  • intersectionRatio (should be decreasing from 1.0)
  • boundingClientRect.bottom
  • boundingClientRect.height

So the callback ended up look like:

intersectionObserver = new IntersectionObserver(function(entries) {
  const entry = entries[0]; // observe one element
  const currentRatio = intersectionRatio;
  const newRatio = entry.intersectionRatio;
  const boundingClientRect = entry.boundingClientRect;
  const scrollingDown = currentRatio !== undefined && 
    newRatio < currentRatio &&
    boundingClientRect.bottom < boundingClientRect.height;

  intersectionRatio = newRatio;

  if (scrollingDown) {
    // it's scrolling down and observed image started to hide.
    // so do something...
  }

  console.log(entry);
}, { threshold: [0, 0.25, 0.5, 0.75, 1] });

See my post for complete codes.

2
  • The answer only works if the element is scrolling off the page. scollingDown will, according to my test, be false while the element is entering the view from the bottom. Commented Jul 1, 2019 at 11:56
  • @RickardElimää your observation seems correct. As I clearly said, my code was a shortcut to capture the moment when the image was scrolling down and started to hide by remembering ratio. If you are interested in both directions, you would have to monitor Y value as well.
    – bob
    Commented Jul 2, 2019 at 21:25
1

:)

I don't think this is possible with a single threshold value. You could try to watch out for the intersectionRatio which in most of the cases is something below 1 when the container leaves the viewport (because the intersection observer fires async). I'm pretty sure that it could be 1 too though if the browser catches up quickly enough. (I didn't test this :D )

But what you maybe could do is observe two thresholds by using several values. :)

threshold: [0.9, 1.0]

If you get an event for the 0.9 first it's clear that the container enters the viewport...

Hope this helps. :)

4
  • threshold doesn't distinguish the direction, instead, it's base on the viewport and also it depends on the rootMargin, so this is kind of unstable solution, furthermore, a 0px height element will have random behavior while triggering the event, I guess a combination of threshold and boundingClientRect would work, I will try it Commented Sep 29, 2017 at 13:08
  • Aah... is misread. :D May I ask why the scroll direction matters in your case? Commented Oct 1, 2017 at 15:35
  • as I said! in the post 'so I can apply this on fixed headers menu to show/hide it' Commented Oct 1, 2017 at 15:39
  • this doesnt correspond to the way intersectionObserver actually works
    – oldboy
    Commented Jul 12, 2023 at 18:29
0

The simplest way to do this would be to set a threshold value of 0.5, which uses the center of the element as a single trigger point, and then monitor intersectionRect.y.

When the element is observed, if intersectionRect.y is 0, this means the element crosses the top of the bounding box; whereas, if it is greater than 0, the element crosses the bottom of the bounding box.

You must also monitor the previous crossing (i.e. via outside) to determine whether or not the element is inside or outside the bounding box because the boolean value of intersectionRect.y must be inverted if the element exits the bounding box.

const
  observer = new IntersectionObserver(observedArr => {
    let
      direction = outside ? observedArr[0].intersectionRect.x : !observedArr[0].inersectionRect.y
    if (direction) // to bottom
    else // to top
    outside = !outside
  }, { threshold: .5 })

let
  outside = false

Not the answer you're looking for? Browse other questions tagged or ask your own question.