IntersectionObserver [JS]

Hello, I'm attempting to use IntersectionObserver() in JS to create a lazy loading effect. My code at the moment is the basic setup. Where I'm stuck at the moment is trying to figure out how to detect the child of the element being observed. Basically, I want to check if .cards is observed and if true, console.log which card is within the intersection. Thanks πŸ‘ Code: https://codepen.io/Matt-CopOffMatt/pen/poqZzjP
102 Replies
ἔρως
ἔρως‒14mo ago
you're observing the parent, not the children you have to observe the children
Matt
MattOPβ€’14mo ago
I tried that with:
let card = document.querySelectorAll(".card")

document.addEventListener("scroll", () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
console.log(entry)
})
})

observer.observe(card)
})
let card = document.querySelectorAll(".card")

document.addEventListener("scroll", () => {
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
console.log(entry)
})
})

observer.observe(card)
})
Uncaught TypeError: Failed to execute 'observe' on 'IntersectionObserver': parameter 1 is not of type 'Element'. at HTMLDocument.<anonymous>
ἔρως
ἔρως‒14mo ago
weird
Matt
MattOPβ€’14mo ago
That's why I figured I had to do the parent then get children could this be happening because querySelectorAll returns a nodelist not an array?
Matt
MattOPβ€’14mo ago
No description
Chooβ™šπ•‚π•šπ•Ÿπ•˜
There are several problems with your code: 1) None of that code should be inside an event listener for scroll. Doing that would create a new observer for each scroll action. 2) Do not observe the parent when you actually want to know if the children are intersecting. Observe the children instead. 3) You must test if the entry is intersecting. The callback runs on all entries whenever any of them intersects. 4) The thing to log or do anything else with is the entry.target and not the entry.
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if(entry.isIntersecting) console.log(entry.target);
});
});
const everyCard = document.querySelectorAll(".card");
everyCard.forEach(card=>{
observer.observe(card);
});
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if(entry.isIntersecting) console.log(entry.target);
});
});
const everyCard = document.querySelectorAll(".card");
everyCard.forEach(card=>{
observer.observe(card);
});
ἔρως
ἔρως‒14mo ago
yeah, that's a good point i didnt read the code thruougly why do you have an intersection observer in a scroll event? entry.target is used for other observers, by the way
Matt
MattOPβ€’14mo ago
It wasn't running continously, only when the intersection was found
Chooβ™šπ•‚π•šπ•Ÿπ•˜
The problem isn't continuous running. The problem is multiple new observers get created when you don't need them.
Matt
MattOPβ€’14mo ago
Yeah. When I originally ran the test code (without scroll event) it was only returning once
Chooβ™šπ•‚π•šπ•Ÿπ•˜
That is because you observed the parent and there is only one of them.
Matt
MattOPβ€’14mo ago
The code you provided though works exactly how I was trying Wouldn't it return once exiting the parent though ?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
What does that even mean?
Matt
MattOPβ€’14mo ago
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
console.log(entry)
})
})

observer.observe(document.querySelector(".cards"))
const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
console.log(entry)
})
})

observer.observe(document.querySelector(".cards"))
There is a starting hero section that takes up entire viewport. When I run this code, it instantly returns despite not ever scrolling to cards section
No description
Matt
MattOPβ€’14mo ago
From my understanding of this, console.log(entry) should fire once scrolled to .cards element but in this snippet video, it fires on page load despite me not scrolling to the .cards section?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Your code doesn't check if it's intersecting and it also doesn't check the intersection ratio.
Matt
MattOPβ€’14mo ago
Ah so basically, what I wrote just check to see if it's within the DOM?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Not exactly. The callback runs once when an element is observed and again when any element being observed is intersecting.
Matt
MattOPβ€’14mo ago
okay Thanks for the explanation I appreciate it
ἔρως
ἔρως‒14mo ago
by the way, don't do lots of processing inside an intersection handler
Matt
MattOPβ€’14mo ago
I'm currently making a simple Lazy Loading demo I'm running into a small issue right now when adding in options
ἔρως
ἔρως‒14mo ago
which issue?
Matt
MattOPβ€’14mo ago
https://codepen.io/Matt-CopOffMatt/pen/poqZzjP When the page loads, the first img is preloading without animating
ἔρως
ἔρως‒14mo ago
that's good
Matt
MattOPβ€’14mo ago
I added in options to try and prevent this, to force it to be within view by setting threshold to 1.0
ἔρως
ἔρως‒14mo ago
you fake it you use loading="lazy" if the image loads too quickly, you fake it
Matt
MattOPβ€’14mo ago
Why would it load though if it's outside of observer, which includes a threshold? I might be misunderstanding the mozilla doc
Chooβ™šπ•‚π•šπ•Ÿπ•˜
The first one is not outside of the root. The root is the viewport. Maybe you are assuming incorrectly that the root is the parent?
Matt
MattOPβ€’14mo ago
From my understanding, my code removes/adds class based on the intersection of child cards returned from the .cards parent
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
Why would it ignore the first card if it should function the same across all child cards?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
In what way is it ignoring the first card? I see the exact same behavior on all of them.
ἔρως
ἔρως‒14mo ago
images load no matter what
Matt
MattOPβ€’14mo ago
Also, I just noticed. I passed options parameters here:
everyCard.forEach(card => {
observer.observe(card, options);
});
everyCard.forEach(card => {
observer.observe(card, options);
});
Should options go here? const observer = new IntersectionObserver(entries, options => {
Chooβ™šπ•‚π•šπ•Ÿπ•˜
The first one is already intersecting the viewport on page load.
Matt
MattOPβ€’14mo ago
How so, if there's an element taking up 100VH?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
You didn't put that in your codepen version.
Matt
MattOPβ€’14mo ago
or is that element not truely taking up the full viewport height ?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
You mentioned before that you have a 100vh hero, but it's not in your codepen.
Matt
MattOPβ€’14mo ago
Updated, I apologized I missed it when copying
Chooβ™šπ•‚π•šπ•Ÿπ•˜
And it behaves correctly after you added that.
Matt
MattOPβ€’14mo ago
Here's what I see
Chooβ™šπ•‚π•šπ•Ÿπ•˜
It fades in for me. Maybe you didn't reload the page. It only fades in once.
Matt
MattOPβ€’14mo ago
Hmm I think it has to do with the 100VH I set main-hero to 110VH and it worked
ἔρως
ἔρως‒14mo ago
use 100dvh not 100vh
Matt
MattOPβ€’14mo ago
Ahh okay
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Try chaning it back to 100vh but ensure that you scroll back to the top and reload.
Matt
MattOPβ€’14mo ago
Just curious, if I were to keep 100VH is there a way to solve this with options? I have been, I don't think it recognizes true 100VH
Chooβ™šπ•‚π•šπ•Ÿπ•˜
It should work with 100vh. It worked in my browser with your code before you changed it to 110vh. But it only works once until the page is reloaded.
Matt
MattOPβ€’14mo ago
100dvh didnt work either Is this something that can be solved with options?
ἔρως
ἔρως‒14mo ago
define "it"
Chooβ™šπ•‚π•šπ•Ÿπ•˜
You can try changing the root margin, but it shouldn't be necessary.
ἔρως
ἔρως‒14mo ago
what do you want from it?
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Did you use Firefox? I just noticed that it works correctly in Chrome but doesn't work correctly in Firefox.
Matt
MattOPβ€’14mo ago
First child should load similarly to the other three. Yeah
Chooβ™šπ•‚π•šπ•Ÿπ•˜
I just realized it's not the browser. There is something about the window size. I am investigating.
ἔρως
ἔρως‒14mo ago
he has a display: none set the opacity to 0 remove the class and use a transition also, use blur too
Matt
MattOPβ€’14mo ago
I was going to eventually add in img src instead of display: none but I'm just confused why it wouldn't work the same as it does for 3/4 of the images
Chooβ™šπ•‚π•šπ•Ÿπ•˜
I determined that it stops working correctly if the window height is small. This might actually be a codepen issue. I am doing a test without Codepen to verify.
Matt
MattOPβ€’14mo ago
I'm experiencing it outside of codepen
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Add a 1px margin-bottom to the hero. I confirmed in multiple tests that adding 1px margin-bottom to the hero fixes it on all screen sizes.
ἔρως
ἔρως‒14mo ago
instead of 1px margin, set it to 101dhv and 101vh
Matt
MattOPβ€’14mo ago
Yeah that's the same as changing the height I probably wont run into this on something beside this demo
Chooβ™šπ•‚π•šπ•Ÿπ•˜
Yes, but it is changing by the minimum amount needed to fix it.
ἔρως
ἔρως‒14mo ago
or, add some padding to the 2nd div, with the images
Matt
MattOPβ€’14mo ago
Am I passing in options incorrectly? Would something like this fix the issue?
let options = {
rootMargin: "10px",
};

const observer = new IntersectionObserver((entries, options) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
});
});

const everyCard = document.querySelectorAll(".card");

everyCard.forEach(card => {
observer.observe(card);
});
let options = {
rootMargin: "10px",
};

const observer = new IntersectionObserver((entries, options) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
});
});

const everyCard = document.querySelectorAll(".card");

everyCard.forEach(card => {
observer.observe(card);
});
Matt
MattOPβ€’14mo ago
options doesn't seem to be defining so I think im doing something wrong
No description
ἔρως
ἔρως‒14mo ago
are you padding an object to the intersection observer? no, you arent
ἔρως
ἔρως‒14mo ago
Intersection Observer API - Web APIs | MDN
The Intersection Observer API provides a way to asynchronously observe changes in the intersection of a target element with an ancestor element or with a top-level document's viewport.
Matt
MattOPβ€’14mo ago
That's where I'm reading I thought it added a margin around the intersection screen size, where it would act similarly to adding margin to say the images for example
Chooβ™šπ•‚π•šπ•Ÿπ•˜
The options is in the wrong place. You made that the second parameter of the callback. It's supposed to be the second parameter of the InterSectionObserver.
Matt
MattOPβ€’14mo ago
Ahh okay thank you Doesn't work as intented maybe im misunderstanding mozilla Root Margin: This set of values serves to grow or shrink each side of the root element's bounding box before computing intersections. Shouldn't this act as adding a margin around the "observer" view? Aka, the viewport size?
ἔρως
ἔρως‒14mo ago
no just makes it "bigger" or "smaller" for calculations the element, not the viewport
Chooβ™šπ•‚π•šπ•Ÿπ•˜
I just noticed some problems with your code. I don't know if you now have something different from what is on Codepen, but the codepen version puts the options in the wrong place. Also, you don't actually need a root margin. You need to change the threshold to something like 0.2. Right now, the threshold isn't being used because the options are in the wrong place.
Matt
MattOPβ€’14mo ago
I just updated that actually. I was trying to figure out where to put that callback
ἔρως
ἔρως‒14mo ago
you are observing if an element intersects the viewport, not if the viewport intersects an element
Chooβ™šπ•‚π•šπ•Ÿπ•˜
let options = {
threshold: 0.2
};

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
});
}, options);

const everyCard = document.querySelectorAll(".card");

everyCard.forEach(card => {
observer.observe(card);
});
let options = {
threshold: 0.2
};

const observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.querySelector("img").classList.remove("isHidden")
entry.target.querySelector("img").classList.add("fadeIn")
};
});
}, options);

const everyCard = document.querySelectorAll(".card");

everyCard.forEach(card => {
observer.observe(card);
});
Matt
MattOPβ€’14mo ago
Sorry im still relatively new to JS Ahh makes sense nows, that's where it's suppose to go Basically const observer = new IntersectionObserver(callback => {} options)
Chooβ™šπ•‚π•šπ•Ÿπ•˜
The options isn't a callback.
Matt
MattOPβ€’14mo ago
Okay true that also makes sense callback, options* I apologize
ἔρως
ἔρως‒14mo ago
yes, that's why i sent the documentation which explains this already
Matt
MattOPβ€’14mo ago
That's what I've been reading Just get confused with some of these explanations
ἔρως
ἔρως‒14mo ago
it is a bit convoluted, but that part is always clearly explained it is the only part that's clear in it what you had before was a callback that took 2 arguments
Matt
MattOPβ€’14mo ago
This was the explanation I sent here
ἔρως
ἔρως‒14mo ago
instead of passing a callback and a 2nd object
Matt
MattOPβ€’14mo ago
made me believe that it adds margin to the observer. Therefore, my logic was that adding 1em, for example, would allow for a 1em gap before recognizing an observation
ἔρως
ἔρως‒14mo ago
rait wait "root element" that's the same as :root? no, it isn't it's for the root option
Matt
MattOPβ€’14mo ago
Which should be the card (container), no ?
ἔρως
ἔρως‒14mo ago
no
root The element that is used as the viewport for checking visibility of the target. Must be the ancestor of the target. Defaults to the browser viewport if not specified or if null.
Matt
MattOPβ€’14mo ago
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
let options = {
root: document.querySelector("#scrollArea"),
rootMargin: "0px",
threshold: 1.0,
};
I thought so because they defined the root as the id scrollArea. I thought in my code example, the cards would be considered the scroll area since thats what's being observed viewport makes more sense was just confused why theyre using an element over viewport
ἔρως
ἔρως‒14mo ago
yes, but you aren't observing .cards but each .card
Matt
MattOPβ€’14mo ago
I'll need to read more about this api to fully understand this is my first attempt at trying to use it so im still trying to understand I appreciate both of your help πŸ‘ thank you@ChooKing@ἔρως
ἔρως
ἔρως‒14mo ago
you're welcome
b1mind
b1mindβ€’14mo ago
If you are not apposed to using a library, GSAP.ScrollTrigger is amazing to leverage IO.
b1mind
b1mindβ€’14mo ago
Matt
MattOPβ€’14mo ago
Tbh, I need to stick to strictly JS right now just need to get a grip been trying to learn consistently for around 1 month now
b1mind
b1mindβ€’14mo ago
yup I get that just figured I'd mention it.
Matt
MattOPβ€’14mo ago
This is very cool btw
b1mind
b1mindβ€’14mo ago
Thanks that was my first time using it, was for a codepen challenge the month they released it. (I had used a scrollMagic lib and IO before though so had some exp in trigger animations)
b1mind
b1mindβ€’14mo ago
hah yea... I still need to make that work infinite scrolling. Thanks!
Want results from more Discord servers?
Add your server