This page is built with the above effect, take a quick scroll to see the effect and if you're interested then read the content on this site or go to the official documentation here.
1
2
3
4
<ul class="stack-cards js-stack-cards">
<li class="stack-cards__item js-stack-cards__item">
<!-- Content here -->
</li>
<li class="stack-cards__item js-stack-cards__item">
<!-- Content here -->
</li>
<!-- additional card items here -->
</ul>
Take note of the used classes and the structure of the list
Scroll down to see required custom css and custom JavaScript
vvvvvv
This is in addition to the CSS of the Dependencies
.stack-cards__item {
position: sticky;
top: var(--space-sm);
transform-origin: center top;
}
var StackCards = function(element) {
this.element = element;
this.items = this.element.getElementsByClassName('js-stack-cards__item');
this.scrollingListener = false;
this.scrolling = false;
initStackCardsEffect(this);
};
function initStackCardsEffect(element) { // use Intersection Observer to trigger animation
var observer = new IntersectionObserver(stackCardsCallback.bind(element));
observer.observe(element.element);
};
var stackCards = document.getElementsByClassName('js-stack-cards'),
intersectionObserverSupported = ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype),
reducedMotion = Util.osHasReducedMotion();
if(stackCards.length > 0 && intersectionObserverSupported && !reducedMotion) {
for(var i = 0; i < stackCards.length; i++) {
new StackCards(stackCards[i]);
}
}
function stackCardsCallback(entries) { // Intersection Observer callback
if(entries[0].isIntersecting) { // cards inside viewport - add scroll listener
if(this.scrollingListener) return; // listener for scroll event already added
stackCardsInitEvent(this);
} else { // cards not inside viewport - remove scroll listener
if(!this.scrollingListener) return; // listener for scroll event already removed
window.removeEventListener('scroll', this.scrollingListener);
this.scrollingListener = false;
}
};
function stackCardsInitEvent(element) {
element.scrollingListener = stackCardsScrolling.bind(element);
window.addEventListener('scroll', element.scrollingListener);
};
function stackCardsScrolling() {
if(this.scrolling) return;
this.scrolling = true;
window.requestAnimationFrame(animateStackCards.bind(this));
};
function animateStackCards() {
var top = this.element.getBoundingClientRect().top;
for(var i = 0; i < this.items.length; i++) {
// cardTop/cardHeight/marginY are the css values for the card top position/height/Y offset
var scrolling = this.cardTop - top - i*(this.cardHeight+this.marginY);
if(scrolling > 0) { // card is fixed - we can scale it down
this.items[i].setAttribute('style', 'transform: translateY('+this.marginY*i+'px) scale('+(this.cardHeight - scrolling*0.05)/this.cardHeight+');');
}
}
this.scrolling = false;
};
There are multiple ways of installing CodyFrame but the following covers how to get needed css/js from the CodyFrame Github repository
Written with access to CodyFrame v4 and v3
1. Navigate to CodyFrame's documentation then select v4 and click on their Github link
2. Navigate to main/css and download all of the css files (individually or as a group) then add them to your project files and link them above the required CSS
3. Now go back to the documentation and select v3 then click on the Github repository link
4. Now navigate to main/assets/js and download the util.js file. Place file in project and link the file above required JavaScript
** You can use other CodyFrame components that are compatible with above downloaded resources
This plugin is not easy to use but allows for a very cool effect
I also found that you have to add padding to the top and bottom of your pages content as they will get cut off or be hard to read as the user scrolls. You may also want to implement a snap scrolling effect to make the pages feel more responsive.
If you plan on on having many heroes reveal at a time then you will need to write css for the z-indexes as bootstrap only has 0-3 and CodyFrame only has 1-4 as provided class options.
As for documentation goes the css provided works but the given js requires you to write the rest of the animateRevealingHero JavaScript function using the variables given in the required code. This makes it difficult to use if you are not aware of the plugins methods bellow is the JavaScript code from the demo which is complete and worked for me.
(function() {
var StackCards = function(element) {
this.element = element;
this.items = this.element.getElementsByClassName('js-stack-cards__item');
this.scrollingFn = false;
this.scrolling = false;
initStackCardsEffect(this);
initStackCardsResize(this);
};
function initStackCardsEffect(element) { // use Intersection Observer to trigger animation
setStackCards(element); // store cards CSS properties
var observer = new IntersectionObserver(stackCardsCallback.bind(element), { threshold: [0, 1] });
observer.observe(element.element);
};
function initStackCardsResize(element) { // detect resize to reset gallery
element.element.addEventListener('resize-stack-cards', function(){
setStackCards(element);
animateStackCards.bind(element);
});
};
function stackCardsCallback(entries) { // Intersection Observer callback
if(entries[0].isIntersecting) {
if(this.scrollingFn) return; // listener for scroll event already added
stackCardsInitEvent(this);
} else {
if(!this.scrollingFn) return; // listener for scroll event already removed
window.removeEventListener('scroll', this.scrollingFn);
this.scrollingFn = false;
}
};
function stackCardsInitEvent(element) {
element.scrollingFn = stackCardsScrolling.bind(element);
window.addEventListener('scroll', element.scrollingFn);
};
function stackCardsScrolling() {
if(this.scrolling) return;
this.scrolling = true;
window.requestAnimationFrame(animateStackCards.bind(this));
};
function setStackCards(element) {
// store wrapper properties
element.marginY = getComputedStyle(element.element).getPropertyValue('--stack-cards-gap');
getIntegerFromProperty(element); // convert element.marginY to integer (px value)
element.elementHeight = element.element.offsetHeight;
// store card properties
var cardStyle = getComputedStyle(element.items[0]);
element.cardTop = Math.floor(parseFloat(cardStyle.getPropertyValue('top')));
element.cardHeight = Math.floor(parseFloat(cardStyle.getPropertyValue('height')));
// store window property
element.windowHeight = window.innerHeight;
// reset margin + translate values
if(isNaN(element.marginY)) {
element.element.style.paddingBottom = '0px';
} else {
element.element.style.paddingBottom = (element.marginY*(element.items.length - 1))+'px';
}
for(var i = 0; i < element.items.length; i++) {
if(isNaN(element.marginY)) {
element.items[i].style.transform = 'none;';
} else {
element.items[i].style.transform = 'translateY('+element.marginY*i+'px)';
}
}
};
function getIntegerFromProperty(element) {
var node = document.createElement('div');
node.setAttribute('style', 'opacity:0; visbility: hidden;position: absolute; height:'+element.marginY);
element.element.appendChild(node);
element.marginY = parseInt(getComputedStyle(node).getPropertyValue('height'));
element.element.removeChild(node);
};
function animateStackCards() {
if(isNaN(this.marginY)) { // --stack-cards-gap not defined - do not trigger the effect
this.scrolling = false;
return;
}
var top = this.element.getBoundingClientRect().top;
if( this.cardTop - top + this.element.windowHeight - this.elementHeight - this.cardHeight + this.marginY + this.marginY*this.items.length > 0) {
this.scrolling = false;
return;
}
for(var i = 0; i < this.items.length; i++) { // use only scale
var scrolling = this.cardTop - top - i*(this.cardHeight+this.marginY);
if(scrolling > 0) {
var scaling = i == this.items.length - 1 ? 1 : (this.cardHeight - scrolling*0.05)/this.cardHeight;
this.items[i].style.transform = 'translateY('+this.marginY*i+'px) scale('+scaling+')';
} else {
this.items[i].style.transform = 'translateY('+this.marginY*i+'px)';
}
}
this.scrolling = false;
};
// initialize StackCards object
var stackCards = document.getElementsByClassName('js-stack-cards'),
intersectionObserverSupported = ('IntersectionObserver' in window && 'IntersectionObserverEntry' in window && 'intersectionRatio' in window.IntersectionObserverEntry.prototype),
reducedMotion = Util.osHasReducedMotion();
if(stackCards.length > 0 && intersectionObserverSupported && !reducedMotion) {
var stackCardsArray = [];
for(var i = 0; i < stackCards.length; i++) {
(function(i){
stackCardsArray.push(new StackCards(stackCards[i]));
})(i);
}
var resizingId = false,
customEvent = new CustomEvent('resize-stack-cards');
window.addEventListener('resize', function() {
clearTimeout(resizingId);
resizingId = setTimeout(doneResizing, 500);
});
function doneResizing() {
for( var i = 0; i < stackCardsArray.length; i++) {
(function(i){stackCardsArray[i].element.dispatchEvent(customEvent)})(i);
};
};
}
}());