Home

Stacking Cards Effect

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.

Dependencies

Step 1: HTML Structure Required


                <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

Custom CSS and JS

This is in addition to the CSS of the Dependencies

Step 2: Add Required CSS and JS to your files

CSS

                .stack-cards__item {
                    position: sticky;
                    top: var(--space-sm);
                    transform-origin: center top;
                }
                

JavaScript

                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;
                };
                

Getting Dependencies

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

Personal Notes

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);
                        };
                      };
                      }
                  }());