I would like to use Intersection Observer to trigger an animation on each item of my grid layout and use a staggered effect while observing them.
My code doesn't seem to work when I use count
to count all the elements in the grid and then setting an attribute for the delay.
The HTML:
<div class="grid">
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
</div>
The CSS:
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
width: 100%;
}
.grid-item {
width: 100%;
height: 30em;
background-color: #ccc;
transition: background-color .6s ease;
}
.grid-item.is-in-view {
background-color: red;
}
The JS:
const options = {
threshold: 0.5
}
const inViewCallback = entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('is-in-view');
}
});
}
let observer = new IntersectionObserver(inViewCallback,options);
window.addEventListener('DOMContentLoaded', (event) => {
let elements = Array.from(document.getElementsByClassName('js-item'));
for (let element of elements) {
for (let count = 0; count < element.length; count++) {
element[count].setAttribute('style', 'transition-delay: ' + count * 0.5 + 's');
observer.observe(element)
}
}
});
You can see the codepen.
Thank you.
You need to add the observer object in the loop over the elements node list and then add the call back method inside the IntersectionObserver object and pass the options at the end of the IntersectionObserver method. Then call the observer and pass in the element being looped over.
In the call back method pass in the entry and element, check if the entry isIntersecting and if it is, change the background-colors style, from there your css transition will handle the animation, add an else to handle transition back to gray.
EDIT:
missing the staggered effect (the delay) between each element to animate them one after the other once they are observed.
You can pass the index from the forEach method through the callBack and use that to calculate a staggered effect for each observable element once the IntersectionObserver fires.
Added further comments inside the code snippet.
//callback method, takes the observers entry and the nodelist item as a param
const inViewCallback = (entries, item, i) => {
// forEach entry
entries.forEach((entry, index) => {
// check if it is intersecting
if (entry.isIntersecting) {
// if it is, change the backgroundColor to red
// set the animationDelay using the index coming from the
// forEach method passed through the callBack for the observer
// and calculate a staggered effect
item.style.transitionDelay = `${i * 50}ms`;
item.style.backgroundColor = 'red';
} else {
item.style.transitionDelay = `${i * 50}ms`;
item.style.backgroundColor = 'gray';
}
});
};
// event for DOM load
window.addEventListener('DOMContentLoaded', (event) => {
// get the element node list
const items = document.querySelectorAll('.js-item');
// set your options for the Intersection object
const options = {
threshold: 0.5
};
// iterate over the elements node list
items.forEach((item, i) => {
// define the Observer and define the entry
// inside the forEach loop to get each single node to be affected
const Observer = new IntersectionObserver((entries) => {
// pass the entries and item node into the observer callback
inViewCallback(entries, item, i);
}, options); // pass in the options
// call the Observer and pass in the item node as a parameter
Observer.observe(item);
});
});
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
width: 100%;
margin-top: 100vh;
}
.grid-item {
width: 100%;
height: 40vh;
background-color: #ccc;
/* removed delay from one liner css transition
so it will handle in the JS inline style */
transition: background ease;
}
<div>Scroll down for effect.</div>
<div class="grid">
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
</div>
The problem with the first approach is it links to the index of the elements and if you have a lot of elements, the delay animation will itself start to delay (calculate to a longer delay as the index gets larger).
To fix this you can use a helper array and counter to iterate over, resetting the counter with each new element per new line without overflow. This will ensure that your animation effect for each new line is consistent. If you want to change the delay, just change the values in the delay array.
The below approach however is static and we can improve by adding a more dynamic approach to keep the consistency of the delays.
// define an array with the transitionDelay times
const delays = [50, 100, 150, 200]
// define a counter
let j = 0;
//callback method, takes the observers entry and the nodelist item as a param
const inViewCallback = (entries, item) => {
// forEach entry
entries.forEach((entry) => {
// check if it is intersecting
if (entry.isIntersecting) {
// use the delay array to and counter to get the next delay value
item.style.transitionDelay = `${delays[j]}ms`;
item.style.backgroundColor = 'red';
// reset the counter with each new line using modulus operator
// basically this says if the counter is equal to
// the length of your array reset the counter back to 0
if(j % 4 === 0) j = 0;
// increment counter
j++
} else {
item.style.transitionDelay = `$${delays[j]}ms`;
item.style.backgroundColor = 'gray';
}
});
};
// event for DOM load
window.addEventListener('DOMContentLoaded', (event) => {
// get the element node list
const items = document.querySelectorAll('.js-item');
// set your options for the Intersection object
const options = {
threshold: 0.5
};
// iterate over the elements node list
items.forEach((item) => {
// define the Observer and define the entry
// inside the forEach loop to get each single node to be affected
const Observer = new IntersectionObserver((entries) => {
// pass the entries and item node into the observer callback
inViewCallback(entries, item);
}, options); // pass in the options
// call the Observer and pass in the item node as a parameter
Observer.observe(item);
});
});
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
width: 100%;
margin-top: 100vh;
}
.grid-item {
width: 100%;
height: 50vh;
background-color: #ccc;
/* I reduced this animation delay here, but honestly that is just preference */
transition: background-color .2s ease;
}
<h2 class="margin">Scroll down for effect</h2>
<div class="grid">
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
</div>
A truly dynamic approach would be to divide the width of the child element with the parents width, then pass that number to a function that would return the amount of elements within a row without overflow. Then another function to create a dynamic array of delays using the number of elements calculated within a row as well as a number passed as a pattern for increasing the delay time for each new element in the row.
Then you set the transistionDelay using the array of times and the counter as an index. Use modulus and the arrays.delay to reset the counter.
// get the parent element
const grid = document.querySelector(".grid");
// helper function that returns how many children live wihtin the parent before overflow
// divide the width of the parent with the width of a child and run through Math.floor
const howManyElsToFill = (parent, el) => {
return Math.floor(parent.offsetWidth / el.offsetWidth);
}
// helper function to build an array of delays
// pass in the amount of child elements and a
// number to increment as delays
// example 50, 100, 150, 200
const createDelaysArray = (els, pattern) => {
const result = []
for (let i = 1; i <= els; i++) {
result.push(i * pattern);
}
return result;
}
// define a counter
let j = 0;
//callback method, takes the observers entry and the nodelist item as a param
const inViewCallback = (entries, item) => {
// pass in the parent element and a child element to compare
const elsInRow = howManyElsToFill(grid, item);
// pass in the amount of elements in a row before we get overflow
// pass in a number to begin with, to populate delay array
const delays = createDelaysArray(elsInRow, 50);
// forEach entry
entries.forEach((entry) => {
// check if it is intersecting
if (entry.isIntersecting) {
// use the delay array to and counter to get the next delay value
item.style.transitionDelay = `${delays[j]}ms`;
item.style.backgroundColor = 'red';
// reset the counter with each new line using modulus operator
// basically this says if the counter is equal to
// the length of your array reset the counter back to 0
if (j % delays.length === 0) j = 0;
// increment counter
j++
} else {
item.style.transitionDelay = `${delays[j]}ms`;
item.style.backgroundColor = 'gray';
}
});
};
// event for DOM load
window.addEventListener('DOMContentLoaded', (event) => {
// get the element node list
const items = document.querySelectorAll('.js-item');
// set your options for the Intersection object
const options = {
threshold: 0.5
};
// iterate over the elements node list
items.forEach((item) => {
// define the Observer and define the entry
// inside the forEach loop to get each single node to be affected
const Observer = new IntersectionObserver((entries) => {
// pass the entries and item node into the observer callback
inViewCallback(entries, item);
}, options); // pass in the options
// call the Observer and pass in the item node as a parameter
Observer.observe(item);
});
});
.grid {
display: grid;
/* you can change the amount of repeated columns
here without having to edit the JS */
grid-template-columns: repeat(3, 1fr);
grid-column-gap: 10px;
grid-row-gap: 10px;
width: 100%;
margin-top: 100vh;
}
.grid-item {
width: 100%;
height: 50vh;
background-color: #ccc;
/* removed delay from one liner css transition
so it will handle in the JS inline style */
transition: background ease;
}
<div>Scroll down for effect. Change the CSS grid-template-columns without editing javascript</div>
<div class="grid">
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
<div class="grid-item js-item"></div>
</div>