8

I have a piece of code that adds a different css class to elements depending on whether they're scrolled into or out of the viewport from top or bottom.

It uses the Intersection Observer because it is supposed to handle large amounts of elements better than scroll events.

However, I am facing two problems with this code:

  1. It does not work in Safari (latest version)
  2. It does not work on mobile Apple devices

This is odd because the IntersectionObserver should work fine on Safari and even mobile browsers on iOS.

You can find the code on jsFiddle or see the snippet here:

const config = {
  // Add root here so rootBounds in entry object is not null
  root: document,
  // Margin to when element should take action
  rootMargin: '-50px 0px',
  // Callback will be fired 30 times during intersection 
  threshold: [...Array(30).keys()].map(x => x / 29)
};

let observer = new IntersectionObserver(function(entries, observer) {

  entries.forEach((entry, index) => {
    const element = entry.target;

    // Get root element (document) coords
    const rootTop = entry.rootBounds.top;
    const rootBottom = entry.rootBounds.height;

    // Get div coords
    const topBound = entry.boundingClientRect.top - 50; // margin in config
    const bottomBound = entry.boundingClientRect.bottom;

    let className;

    // Do calculations to get class names
    if (topBound < rootTop && bottomBound < rootTop) {
      className = "outview-top";
    } else if (topBound > rootBottom) {
      className = "outview-bottom";
    } else if (topBound < rootBottom && bottomBound > rootBottom) {
      className = "inview-bottom";
    } else if (topBound < rootTop && bottomBound > rootTop) {
      className = "inview-top";
    }
    element.setAttribute('data-view', className);

  });
}, config);

const viewbox = document.querySelectorAll('.viewme');
viewbox.forEach(image => {
  observer.observe(image);
});
body {
  text-align: center;
}

.margins {
  position: fixed;
  top: 50px;
  bottom: 50px;
  border-top: 2px dashed;
  border-bottom: 2px dashed;
  z-index: 1;
  left: 0;
  width: 100%;
  pointer-events: none;
}

.hi {
  padding: 40vh 0;
  background: lightgray;
}

.box {
  width: 23%;
  min-width: 100px;
  height: 40vh;
  margin-bottom: 10px;
  background: lightblue;
  display: inline-block;
}

.viewme {
  transition: all .3s ease;
}

.viewme[data-view='inview-top'],
.viewme[data-view='inview-bottom'] {
  opacity: 1;
  transform: translateY(0);
}

.viewme[data-view='outview-top'] {
  opacity: 0;
  transform: translateY(-20px);
}

.viewme[data-view='outview-bottom'] {
  opacity: 0;
  transform: translateY(20px);
}
<p class="hi">Scroll down and back up</p>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>
<div class="box viewme"></div>

<div class='margins'>

</div>

So far, I have two hints as to what might cause these problems:

  1. In the Safari developer console, it says there is a Type error between line 10 and 38 in my JS code
  2. I have noticed that other scripts defining root: document do not work on iOS. Instead, they work when defining root: null. However, I can not use root: null because of rootBounds. I've tried to wrap my html in a div and set the id of the div as the root element but that didn't work (see here).

Any help to solve both problems is much appreciated. However, please do consider that I did not write the code above and don't understand it very well.

Jascha Goltermann
  • 1,074
  • 2
  • 16
  • 31

6 Answers6

11

Although I can't put my finger on the exact cause of the bug, I do have a solution:

Try using document.body as the root and define sizes and scroll behavior to both html and body.

I think this relates to document being more than a simple html node (I also tried using document.documentElement without success) and how Safari initializes the box model for it.

Anyway, here's the updated working fiddle https://jsfiddle.net/gion_13/okrcgejt/8/ and screencasts of the tests on iOS and Mac Safari:

enter image description here enter image description here

gion_13
  • 41,171
  • 10
  • 96
  • 108
  • Thanks, that works on jsFiddle. It still doesn't work on my website though which might be because of the way it uses ajax. It wouldn't be fair not to award you the bounty though so I'm doing that. Would you still take a quick look at my site anyway and take a guess? – Jascha Goltermann Jun 03 '20 at 07:37
  • Sure, leave me a message on bogdan.gradinariu [@] gmail.com Thanks for accepting. If someone else totally figures it out, feel free to "unaccept" this answer :) – gion_13 Jun 03 '20 at 08:04
  • You are correct about Safari requiring the `root` option/declaration - without `root`, an error is thrown: `Unhandled Promise Rejection: AbortError: The operation was aborted.` Always pass `root` - thank you for this answer. – gdibble Feb 07 '22 at 03:58
  • Hello, @gion_13 could you take a look at my question? https://stackoverflow.com/questions/75204977/intersectionobserver-to-create-a-lazy-load-images-with-data-srcset-and-imagekit – Sophie Jan 24 '23 at 11:54
6

I couldn't find a solution to your problem but mine got solved when I reduced the threshold from 1 to 0.9. When it was set to 1, intersection observer wasn't working in Safari. But it worked perfectly in Firefox and Chrome.

const options={
    root:null,
    rootMargin:'0px',
    threshold:0.9
 };

It works whether the root is set to null or to document.body. For some reason, Intersection Observer in Safari wasn't working when I request it to only trigger when the object is 100% fully visible,but it works for 90%. I hope this helps someone out there.

Emre Kelleci
  • 79
  • 2
  • 5
3

I think I have found the issue. If your item is too tall (height) for the viewport (screen) the element will never fit the threshold at least it is small enough. That's why I believe the answer from @donquixote is odd but correct.

 const options = {
      root: null,
      rootMargin: "0px",
      threshold: 0.1,
    };

This worked for me.

2

Just an FYI, If you check MDN - https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API#browser_compatibility

root can't be the document in Safari, IE, Safari IOS and Firefox Android. As to why this is, I have no clue, just sharing that this is already a known issue.

Shane Omirro
  • 21
  • 1
  • 2
  • This does not provide an answer to the question. Once you have sufficient [reputation](https://stackoverflow.com/help/whats-reputation) you will be able to [comment on any post](https://stackoverflow.com/help/privileges/comment); instead, [provide answers that don't require clarification from the asker](https://meta.stackexchange.com/questions/214173/why-do-i-need-50-reputation-to-comment-what-can-i-do-instead). - [From Review](/review/late-answers/29969974) – Toni Oct 01 '21 at 17:31
  • This does provide the answer, actually. The problem is that the user did not set `root`, and @Shane Omirro points out the exact problem, and kudos to him for giving a link to the MDN article proving the point. – gdibble Feb 07 '22 at 04:01
0

I have had the same issue recently and to solve this I just had to

set the root: null

0

For me worked document.body and any threshold different then [] || 0.

const options = {
    root: document.body,
    threshold: 0.000001
}
donquixote
  • 365
  • 3
  • 10