Intersection observer firing

I have this code:
const navbar = document.querySelector(".navbar");
const products = document.querySelector("#Products");

const productsOptions = {
rootMargin: "-300px",
};

const productsObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.add("navbar-dark");
}
else {
navbar.classList.remove("navbar-dark");
}
});
}, productsOptions);

productsObserver.observe(products);
const navbar = document.querySelector(".navbar");
const products = document.querySelector("#Products");

const productsOptions = {
rootMargin: "-300px",
};

const productsObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.add("navbar-dark");
}
else {
navbar.classList.remove("navbar-dark");
}
});
}, productsOptions);

productsObserver.observe(products);
For the following webpage: https://myntsu.github.io/e-commerce-sample/landing-page/ It targets the navbar. Is there anyway to make it fire once it reaches #Products and stays like that going downwards? And to keep the class removal when going back. Currently it removes it when entering and leaving that section, with a margin of -300px (both up and down). I tried many options, but no luck.
47 Replies
Joao
Joao•2y ago
It might be easier to not use intersectionobserver in this case. Add an event listener to the window on scroll, and compare the current window.scrollY position with the products container position. If it's less than (you are above), remove class and otherwise (you are below) add it. Usually with scroll events you want to apply some throttle function as they fire many times with each scroll so it can cause performance issues.
~MARSMAN~
~MARSMAN~•2y ago
You could use two observers, one for the production and one for the section under the production. And use only if condition without the else. Something like this:
const navbar = document.querySelector(".navbar");
const products = document.querySelector("#Products");
const sectionUnderProducts = document.querySelector("#section");

const productsOptions = {
rootMargin: "-300px",
};

const productsObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.add("navbar-dark");
}
});
}, productsOptions);

productsObserver.observe(products);


const secondSectionObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.remove("navbar-dark");
}
});
}, sectionOptions);

secondSectionObserver.observe(sectionUnderProducts );
const navbar = document.querySelector(".navbar");
const products = document.querySelector("#Products");
const sectionUnderProducts = document.querySelector("#section");

const productsOptions = {
rootMargin: "-300px",
};

const productsObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.add("navbar-dark");
}
});
}, productsOptions);

productsObserver.observe(products);


const secondSectionObserver = new IntersectionObserver(function(entries){
entries.forEach(entry => {
if (entry.isIntersecting) {
navbar.classList.remove("navbar-dark");
}
});
}, sectionOptions);

secondSectionObserver.observe(sectionUnderProducts );
Sorry if it's a little messy.. writing on my phone 😅
Joao
Joao•2y ago
Two observers... I don't really like this approach for something this simple 🫤
Myndi
Myndi•2y ago
This could cause another issue, I have more sections than this 2.
~MARSMAN~
~MARSMAN~•2y ago
If the section intersects it will stay like this until you scroll back up to the product section You're only using if, no else
Myndi
Myndi•2y ago
I thought of that approach initially, but perceived as inefficient, specially with other observers for the animations 🤔 How do you throttle events? I will do the on scroll function, then can I post it here for you to review it?
~MARSMAN~
~MARSMAN~•2y ago
Idk i feel like these extra couple of lines won't hurt 😬
Joao
Joao•2y ago
function throttle(fn: () => void, timeInMs: number) {
let throttle = false;

return function () {
if (throttle) {
return;
}

fn();
throttle = true;
setTimeout(() => {
throttle = false;
}, timeInMs);
};
}

function someFn() {
console.log('hello');
}

const throttledFn = throttle(someFn, 20);
function throttle(fn: () => void, timeInMs: number) {
let throttle = false;

return function () {
if (throttle) {
return;
}

fn();
throttle = true;
setTimeout(() => {
throttle = false;
}, timeInMs);
};
}

function someFn() {
console.log('hello');
}

const throttledFn = throttle(someFn, 20);
Myndi
Myndi•2y ago
I'm delivering this as a professional page for a client. Wouldn't like to have repeated code for simpler solutions, that would look bad on me, at least don't want to do it in this case sadCat here I would throttle the on scroll, correct? Inside someFn.
Joao
Joao•2y ago
You provide it with a function and that will return another function that even when called repeatedly, will only run every timeInMs miliseconds Yes, on the scroll event. If you just try quickly yourself with a simple console.log, you will see that it will call your function hundreds of times with a little scrolling Normally not an issue, and with your example probably won't cause any issues either. But just in case it's good practice. Specially if you plan on having similar events for other sections of the page
Myndi
Myndi•2y ago
Currently the other events have unobserve, assuming that would help with performance.
Joao
Joao•2y ago
Yes, definitely In this case though you want to monitor the position constantly so that is not an option. I would just use this as it's effective, performant and simple.
Myndi
Myndi•2y ago
I questioned it at first, based on Kevin's video.
Myndi
Myndi•2y ago
Kevin Powell
YouTube
How to change your navigation style on scroll
This video explores using the Intersection Observer API to watch for an element leaving the page and then changing the style of a fixed navigation bar. GitHub repo: https://github.com/kevin-powell/navbar-change-on-scroll (includes start and finished versions) I set up some custom properties ahead of time to make the change really super simple,...
Myndi
Myndi•2y ago
He scrolls down, and it fires only once, and removes it going back up. But he never went further down, so I was questioning if I was doing something wrong or if they changed how the function works.
Joao
Joao•2y ago
You can also if you prefer, add a check for the same thing (window position vs element position) inside the intersection observer callback, when it's intersecting
~MARSMAN~
~MARSMAN~•2y ago
Kevin's observer is observing the home section. Nothing above this section only beneath it. Your observer is observing a section between two sections. So whenever you scroll above this "product" section the nav will change, Same as scrolling down. If there's a section above the home section In Kevin's code, then the same of what's happening to your Product section will happen to his home section. Observer fires once the target is in viewport, or once it reaches the margin you set it to. You will need to do extra work than using an intersection observer to control your nav appearance. Either what Joao suggested or what I did or what others might suggest later.
Myndi
Myndi•2y ago
In his demo the event fires when the top of the screen reaches the top of the observed element, right?
~MARSMAN~
~MARSMAN~•2y ago
In his demo he's using:
If(!entery.isIntersecting){
target.classList.add("class");
}
If(!entery.isIntersecting){
target.classList.add("class");
}
He's setting his condition to if the home section isn't in viewport then the nav appearance will change. The ! In the entery.isIntersecting means if not intersecting.
Myndi
Myndi•2y ago
Yeah, but notice it that it has to be in 100% of the viewport to fire. Contrary to mine, as soon as one pixel sees it, it fires. That's what confuses me 😓
~MARSMAN~
~MARSMAN~•2y ago
You can use "threshold" it sets how much of the target element have to be in the viewport in order for the event to fire. Like 20% (or 50% or whatever number you wish) of the element appears in the viewport then the event will fire. But it has nothing to do with your case.
Myndi
Myndi•2y ago
Yeah, either threshold or root margin, but still doesn't answer my question sadCat his behavior is different than mine. He doesn't have any value to start with, and it fires as soon as the top of his screen reaches the beginning of that section. I feel like there's something weird with mine, definitely.
~MARSMAN~
~MARSMAN~•2y ago
Look, your observer is looking at the product section and once it appears in the viewport then the nav appearance will change. And once it leaves the nav will change back. This works both sides weather up or down from it. You want your nav to change only once the product section appears and stays like that even if you scrolled beneath it, right?
Myndi
Myndi•2y ago
I changed it to the main tag, but it still follows the same behavior. There's definitely something weird. If I set a root higher than 300, it won't work. Or a threshold higher than .2, same as well. And root accepts a margin not higher than the element itself (in pixels), or a % value, as for threshold, it accepts from 0 to 1.
~MARSMAN~
~MARSMAN~•2y ago
Can you add the code in a codepen?
Myndi
Myndi•2y ago
What exactly?
Joao
Joao•2y ago
Does this work like you mean? https://codepen.io/D10f/pen/VwdPEVM
~MARSMAN~
~MARSMAN~•2y ago
Actually nevermind.. I'll make a quick one Well nevermind again lol Joao did it 🙌
Joao
Joao•2y ago
Like I said: effective, performant and simple 😄
~MARSMAN~
~MARSMAN~•2y ago
I like it 👌
Myndi
Myndi•2y ago
Oh, so the throttle is within the event listener itself. I was actually doing the window on scroll, but my stubborn head wanted to make the observer work 😔
Joao
Joao•2y ago
I would recommend storing it as a separate function, I just did it like that in the codepen for simplicity
Myndi
Myndi•2y ago
I still don't understand why the behavior was different. May I use this code instead?
Joao
Joao•2y ago
You must pay me many 💰
Myndi
Myndi•2y ago
The only difference from what I had is the throttle, and bounding (was calculating from the top to the element pepperderp ). I will just do my own then, thanks for the help.
Joao
Joao•2y ago
Darn, worth a try anyway 😂
Myndi
Myndi•2y ago
Regardless, if anyone else pops in here, please help me understanding the observer behavior, that I clearly don't perceive to understand with my use case.
Joao
Joao•2y ago
I think the confusion is because the IntersectionObserver uses the viewport to start detecting intersections. It doesn't check based on element positions, which is what we manually do in the codepen example. That's why you can actually access this same information from within the entry object, inside the callback. The name is a bit misleading, it doesn't actually observe for intersecting elements. It observes for elements that are visible. Which is to say, elements that intersect with the viewport.
Myndi
Myndi•2y ago
Yeah, I understand that part, but that's not when the confusion arises. In my case, it fires as soon as the viewport touches the element. In Kevin's case, it fires as soon as the top of the element reaches the top of the viewport. While we both had similar code. I figured there was something weird, because, in example, the threshold wasn't accepting a value from 0 to 1, just 0 to 0.2, that's why I was so confused myndiUgh
Joao
Joao•2y ago
There's a nuance here with this, although I'm not 100% sure right now, I will have to confirm it later. The thing is that the callback function is invoked when both the element enters and leaves the viewport. Since the example in the video uses if ( ! entry.isIntersecting ), it gives the impression that when the page first loads nothing happens. In fact it does run, but no visible change happens.
Myndi
Myndi•2y ago
Hmm, I tried it as well, also swapping the remove and add, or with toggle. In theory, he has a color theme (which targets the root variables), and swaps it, exactly what I was doing. But in my case I just replaced the background color from transparent to blue.
Joao
Joao•2y ago
Ah I see, in the example the observer is observing the very first section. So when it leaves the viewport it fires the callback I've updated this to include a version with intersection observer: https://codepen.io/D10f/pen/VwdPEVM Notice how in the scroll event example I'm comparing the coordinates of the s2 element which is what we're actually after. It's a more literal approach. In the intersection observer example I'm using, just like in Kevin's video, the s1 element which is already within view to start with. When it leaves the viewport the callback fires. So it's not intersecting the element we actually care about, s2, it's simply leaving the section above. This is also the reason why the navbar doesn't change back when s2 leaves the viewport.
Myndi
Myndi•2y ago
So it would work even without the const for s2. Now the question is, which approach would be better. To observe it, or based on windows thonking
Joao
Joao•2y ago
Yes, I left that there for the example below (commented out). Personally, I prefer window scroll event. It's very simple to do and gives you more control.
Myndi
Myndi•2y ago
In this case it does, but now I understand my fault.
Joao
Joao•2y ago
I use IntersectionObserver when I need to interact with both enter and leave events. Or when I need to load something before reaching a particular element. For example I've used this to load images, and to start/stop videos loopbacks so that they won't play needlessly when they are not being watched (since they are out of viewport)
Myndi
Myndi•2y ago
I was treating it from the incorrect approach. The event fires accordingly and appropriately, but the first element was always visible on screen. So it was doing a reverse effect, when it reality, if it had more content above, it would have done it like my use case. That makes it more clear, and now I understand it better, thank you for the help though. Very insightful, if I had pennies to spare, wouldn't doubt compensating for it. Keep at it though, cheers!