I want to trigger CSS animations in a horizontal layout, only when the animated element gets in the viewport. I would also like to have them triggered every time they come into the viewport, not just once.
The JavaScript Intersection Observer API can watch for elements positions in any scroll direction, can perform actions on them when they enter/leave the viewport, and can repeat those actions each time these events happen. It is very efficient and well-supported. Here is a basic example:
// Get an array of elements to watch
let elements = document.querySelectorAll('.foo');
// Set an observer to watch their position relative to the viewport boundaries
let observer = new IntersectionObserver(function (entries, self) {
entries.forEach(entry => {
// If this item is in the viewport
if (entry.isIntersecting) {
// Do some code on that item
entry.target.classList.toggle('animated');
}
});
}, { rootMargin: '0px 0px 0px 0px' });
// Tell each element to be watched
elements.forEach(el => {
observer.observe(el);
});
Related
I try do a webite with different divs or for me they are sections. If I reached the top of one of these it should console log this term. If u ask, ScrollHeight is equal to 1% of the devices' screenheight.
let Point1 = false;
document.addEventListener("scroll", e=> {
if (document.documentElement.scrollTop >= 150*ScrollHeight) {
if (Point1 == false){
Point1 = true;
Point1F();
};
}
})
function Point1F() {
console.log("U've done it');
}
But its not woking for me.
Your code works, as i think the problem why you don't see your .log() is because you didn't reach it.
If scrollHeight is (as you said) "1% of the devices' screenheight", then you need html height to be ~ 3x your screen height;
document.documentElement.style.height = "300vh";
// getting 1% of screen height
const scrollHeight = screen.height / 100;
const scrollTriggerPoint = scrollHeight * 150;
let point1 = false;
document.addEventListener("scroll", (e) => {
if (document.documentElement.scrollTop >= scrollTriggerPoint) {
if (point1 == false){
point1 = true;
point1F();
};
}
});
function point1F() {
console.log("u've done it");
}
P.S.
Don't use variable's/function's names starting with a capital letter, use it on;y for constructor functions or classes.
Intersection Observer API
Using scroll position is fine when you have a single trigger point. However, when there are multiple trigger points (as the question suggests) and they are not in a consistent position on different devices, then the Intersection Observer API is a useful solution.
MDN:
Implementing intersection detection in the past involved event
handlers and loops calling methods like
Element.getBoundingClientRect() to build up the needed information for
every element affected. Since all this code runs on the main thread,
even one of these can cause performance problems. When a site is
loaded with these tests, things can get downright ugly.
You create an observer on the document or a container element and then add the elements you want to watch. And the callback is triggered when an element reaches the threshold setting.
Demo Snippet
The snippet shows how to observe different sections as they scroll in and out of view.
// create an observer on the document or container element
let observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
// code to execute when the section becomes visible
console.log("is visible: " + entry.target.id);
// uncomment to trigger only once per section
// observer.unobserve(entry.target);
}
}, {
root: document, // or container element or null
rootMargin: "0px",
threshold: 0.1
});
// add each section to the observer
document.querySelectorAll("section").forEach(target => {
observer.observe(target);
});
section {
height: 5em;
margin: 1em;
margin-bottom: 20em;
background-color: lightblue;
}
Scroll down the page to trigger the observer
<section id="section1">Section 1</section>
<section id="section2">Sectopm 2</section>
<section id="section3">Section 3</section>
<section id="section4">Section 4</section>
<section id="section5">Section 5</section>
I'm very new to the IntersectionObserver API, and I've been experimenting with this code:
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
function callback() {
console.log('observer triggered.');
}
This seems to work as it should, and callback() is called whenever .lazy-load element enters the viewport, but callback() also fires once when the page is initially loaded, which triggers `console.log('observer triggered.');
Is there a reason for this callback to be triggered when the page loads? Or is there a mistake in how I'm implementing this?
Edit: Altering the code to the below still fires the callback at page load.
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let callback = function(entries, observer) {
entries.forEach(entry => {
console.log('observer triggered.');
});
};
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
That is the default behaviour. When you instantiate an instance of the IntersectionObserver, the callback will be fired.
It is recommended to guard against this case.
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.classList.add('in-viewport');
} else {
entry.target.classList.remove('in-viewport');
}
});
Also I found this article as well as the docs to be very helpful, specifically about the intersectionRatio or isIntersecting properties on the IntersectionObserverEntry.
· https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
as easy as it sounds I was able to fix the issue by
adding a threshold comparison condition
adding a slight delay for initialization of observer
const options = {
threshold: 1.0,
};
setTimeout(() => {
observer = new IntersectionObserver(([entry]) => {
console.log("OBSERVER TRIGGERED 1");
if (
entry &&
entry.isIntersecting &&
entry.intersectionRatio >= options.threshold
) {
console.log("OBSERVER TRIGGERED 2");
}
}, options);
observer.observe(observerRef.value);
}, 2000);
I would also suggest temporary changing the background color for observable element to something like:
.observer {
background-color: red;
}
and doing the page refresh. This way your might actually see the red background flashing on your screen hence triggering the event.
Now, before you throw tomatoes at me - in my case - I have a dozen of videos on the webpage. The video HTML elements are not "expanded" right away, because browser needs to download information about the poster images. Hence the page was loaded but vides were still loading one by one.. Adding a slight delay fixed the issue so the browser had time to expand the video contents.
The Situation
I have a fixed nav bar at the top of the page. As you scroll down through different sections of the page the nav bar dynamically updates (underlines and highlights). You can also click a section on the nav bar and it will scroll down to that section.
This is done using the intersection observer API to detect which section it's on and scrollIntoView to scroll to each section.
The Problem
Lets say you are on section 1 and you click the last section, 5, and it scrolls the page down past all the other sections in-between. The scroll is fast and as it scrolls all the sections are detected by the intersection observer and therefore the nav is updated. You end up getting an effect of the nav quickly changing for each nav item as it goes past each corresponding section.
The Goal
How do you delay the intersection observer from triggering the menu change if the section is only in frame for a millisecond? When quickly scrolling the nav bar should only update once the scrolling has stopped on a section.
Code Setup
const sectionItemOptions = {
threshold: 0.7,
};
const sectionItemObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
// select navigation link corresponding to section
} else {
// deselect navigation link corresponding to section
}
});
}, sectionItemOptions);
// start observing all sections on page
sections.forEach((section) => {
sectionItemObserver.observe(section);
});
Ideas
My first thought was to put a setTimeout so that the nav wouldn't change until the Timeout was finished, then cancel the Timeout if the section left the screen before the timeout finished. But as the timeout is in a forEach loop this didn't work.
const sectionItemObserver = new IntersectionObserver((entries, observer) => {
entries.forEach((entry) => {
let selectNavTimeout
if (entry.isIntersecting) {
// Set timeout when section is scrolled past
selectNavTimeout = setTimeout(() => {
// select navigation link corresponding to section
}, 1000)
} else {
// deselect navigation link corresponding to section
// cancel timeout when section has left screen
clearTimeout(selectNavTimeout)
}
});
}, sectionItemOptions);
Any other ideas would be greatly appreciated! Thanks :)
I had the same problem. I end up use the setTimeout approach. You need to associate the timeouts with the entry target, provided each entry target has some unique ID. For example, suppose we are intersecting nodes with id property:
let timeouts = {};
const observer = new IntersectionObserver((entries, ob) => {
for (const e of entries) {
if (e.isIntersecting) {
timeouts[e.target.id] = setTimeout(() => {
ob.unobserve(e.target)
// handling
}, 1000) // delay for 1 second
} else {
clearTimeout(timeouts[e.target.id])
}
}
}, options)
Ran into same issue. Per this article: https://web.dev/intersectionobserver-v2/, observer v2 allows you to set a delay in the observer options. In my nav menu situation the delay works like a charm:
const observer = new IntersectionObserver((changes) => {
for (const change of changes) {
// ⚠️ Feature detection
if (typeof change.isVisible === 'undefined') {
// The browser doesn't support Intersection Observer v2, falling back to v1 behavior.
change.isVisible = true;
}
if (change.isIntersecting && change.isVisible) {
visibleSince = change.time;
} else {
visibleSince = 0;
}
}
}, {
threshold: [1.0],
// 🆕 Track the actual visibility of the element
trackVisibility: true,
// 🆕 =====ANSWER=====: Set a minimum delay between notifications
delay: 100
}));
After lots of brainstorming I came up with an idea that didn't exactly answer the question of delaying the Intersection Observer API but it did solve the problem of the nav bar flickering.
The highlighting of the nav item is done through adding an "is-active" class onto it and then applying CSS to it. Because the "is-active" class is only on the nav item for a split second you can use CSS keyframes to delay the application of CSS styles. By the time the delay has finished the "is-active" class isn't present on the nav item and no styles are changed.
Keeping the original JS the same this is the CSS used
.is-active {
animation: navItemSelected;
animation-duration: 0.3s;
// delay longer than the time nav item is in frame
animation-delay: 0.1s;
// fill mode to hold animation at the end
animation-fill-mode: forwards;
}
#keyframes navItemSelected {
// default non-selected style of nav item
from {
font-style: normal;
opacity: 0.5;
}
// highlighted style of nav item
to {
font-style: italic;
opacity: 1;
}
}
I want to implement scrolling event function that forwards me to certain position on page like it's done in FullPage.js.
Example: https://alvarotrigo.com/fullPage
I tried adding onscroll event listener, which distinguishes scrolling direction and then executes scrollTo() , but it seems like a bad idea.
How do i implement this properly? Thanks in advance.
What i tried so far:
function throttle(fn, delay) {
let last;
let timer;
return () => {
const now = +new Date;
if (last && now < last + delay) {
clearTimeout(timer);
timer = setTimeout(() => {
last = now;
fn();
}, delay);
} else {
last = now;
fn();
}
};
}
var scrollPos = 0;
function smooth_scroll(){
// detects new state and compares it with the new one
if ((document.body.getBoundingClientRect()).top > scrollPos)
{
window.scrollTo({ top: 0, behavior: 'smooth' })
}
else
{
window.scrollTo({ top: 1000, behavior: 'smooth' })//as an example
}
// saves the new position for iteration.
scrollPos = (document.body.getBoundingClientRect()).top;
}
window.addEventListener('scroll', throttle(smooth_scroll, 1000));
I expect it to work like this: whenever i scroll down it forwards me to bottom (1000, 0), and when i scroll up it gets me to the top. All smoothly.
If you look onto your example website you can see that they are not scrolling a list. They are animating a wrapper and its children:
class: fullpage-wrapper
attribute they change: transform:translate3d(0px, -937px, 0px);
What happens ist that all elements are inside one wrapper which is then moved upwards the height of the box. The smooth effect of this page is provided by a specific easing which can be attached to the CSS animation.
So if you want to get the same "scrolling" effect you need to think about an animation solution. Therefore you can stick to blank CSS-Animations or Framework/Library specific ones.
For further informations about animation, I can try to help you as far as it's not getting a full Animation course :)
I'm very new to the IntersectionObserver API, and I've been experimenting with this code:
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
function callback() {
console.log('observer triggered.');
}
This seems to work as it should, and callback() is called whenever .lazy-load element enters the viewport, but callback() also fires once when the page is initially loaded, which triggers `console.log('observer triggered.');
Is there a reason for this callback to be triggered when the page loads? Or is there a mistake in how I'm implementing this?
Edit: Altering the code to the below still fires the callback at page load.
let target = document.querySelector('.lazy-load');
let options = {
root: null,
rootMargin: '0px',
threshold: 0
}
let callback = function(entries, observer) {
entries.forEach(entry => {
console.log('observer triggered.');
});
};
let observer = new IntersectionObserver(callback, options);
observer.observe(target);
That is the default behaviour. When you instantiate an instance of the IntersectionObserver, the callback will be fired.
It is recommended to guard against this case.
entries.forEach(entry => {
if (entry.intersectionRatio > 0) {
entry.target.classList.add('in-viewport');
} else {
entry.target.classList.remove('in-viewport');
}
});
Also I found this article as well as the docs to be very helpful, specifically about the intersectionRatio or isIntersecting properties on the IntersectionObserverEntry.
· https://www.smashingmagazine.com/2018/01/deferring-lazy-loading-intersection-observer-api/
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserver
· https://developer.mozilla.org/en-US/docs/Web/API/IntersectionObserverEntry
as easy as it sounds I was able to fix the issue by
adding a threshold comparison condition
adding a slight delay for initialization of observer
const options = {
threshold: 1.0,
};
setTimeout(() => {
observer = new IntersectionObserver(([entry]) => {
console.log("OBSERVER TRIGGERED 1");
if (
entry &&
entry.isIntersecting &&
entry.intersectionRatio >= options.threshold
) {
console.log("OBSERVER TRIGGERED 2");
}
}, options);
observer.observe(observerRef.value);
}, 2000);
I would also suggest temporary changing the background color for observable element to something like:
.observer {
background-color: red;
}
and doing the page refresh. This way your might actually see the red background flashing on your screen hence triggering the event.
Now, before you throw tomatoes at me - in my case - I have a dozen of videos on the webpage. The video HTML elements are not "expanded" right away, because browser needs to download information about the poster images. Hence the page was loaded but vides were still loading one by one.. Adding a slight delay fixed the issue so the browser had time to expand the video contents.