Будем создавать эффект складывания блоков при прокрутке друг на друга (стекирование).
Вёрстка
<div class="stack-cards js-stack-cards">
<div class="stack-cards__item js-stack-cards__item" id="card-1"><div>
01
</div></div>
<div class="stack-cards__item js-stack-cards__item" id="card-2"><div>
02
</div></div>
<div class="stack-cards__item js-stack-cards__item" id="card-3"><div>
03
</div></div>
<div class="stack-cards__item js-stack-cards__item" id="card-4"><div>
04
</div></div>
</div>
Стили
.stack-cards {
--stack-cards-gap: 20px;
}
.stack-cards__item {
position: -webkit-sticky;
position: sticky;
top: 110px;
height: 0;
padding-bottom: 50%;
-webkit-transform-origin: center top;
transform-origin: center top;
overflow: hidden;
background: #fff1f0;
border-radius: 30px;
box-shadow: 0 1px 10px rgba(0, 0, 0, .075);
}
Скрипт
// Utility function
function Util () {};
/*
class manipulation functions
*/
Util.hasClass = function(el, className) {
if (el.classList) return el.classList.contains(className);
else return !!el.className.match(new RegExp('(\\s|^)' + className + '(\\s|$)'));
};
Util.addClass = function(el, className) {
var classList = className.split(' ');
if (el.classList) el.classList.add(classList[0]);
else if (!Util.hasClass(el, classList[0])) el.className += " " + classList[0];
if (classList.length > 1) Util.addClass(el, classList.slice(1).join(' '));
};
Util.removeClass = function(el, className) {
var classList = className.split(' ');
if (el.classList) el.classList.remove(classList[0]);
else if(Util.hasClass(el, classList[0])) {
var reg = new RegExp('(\\s|^)' + classList[0] + '(\\s|$)');
el.className=el.className.replace(reg, ' ');
}
if (classList.length > 1) Util.removeClass(el, classList.slice(1).join(' '));
};
Util.toggleClass = function(el, className, bool) {
if(bool) Util.addClass(el, className);
else Util.removeClass(el, className);
};
Util.setAttributes = function(el, attrs) {
for(var key in attrs) {
el.setAttribute(key, attrs[key]);
}
};
/*
DOM manipulation
*/
Util.getChildrenByClassName = function(el, className) {
var children = el.children,
childrenByClass = [];
for (var i = 0; i < el.children.length; i++) {
if (Util.hasClass(el.children[i], className)) childrenByClass.push(el.children[i]);
}
return childrenByClass;
};
Util.is = function(elem, selector) {
if(selector.nodeType){
return elem === selector;
}
var qa = (typeof(selector) === 'string' ? document.querySelectorAll(selector) : selector),
length = qa.length,
returnArr = [];
while(length--){
if(qa[length] === elem){
return true;
}
}
return false;
};
/*
Animate height of an element
*/
Util.setHeight = function(start, to, element, duration, cb) {
var change = to - start,
currentTime = null;
var animateHeight = function(timestamp){
if (!currentTime) currentTime = timestamp;
var progress = timestamp - currentTime;
var val = parseInt((progress/duration)*change + start);
element.style.height = val+"px";
if(progress < duration) {
window.requestAnimationFrame(animateHeight);
} else {
cb();
}
};
//set the height of the element before starting animation -> fix bug on Safari
element.style.height = start+"px";
window.requestAnimationFrame(animateHeight);
};
/*
Smooth Scroll
*/
Util.scrollTo = function(final, duration, cb, scrollEl) {
var element = scrollEl || window;
var start = element.scrollTop || document.documentElement.scrollTop,
currentTime = null;
if(!scrollEl) start = window.scrollY || document.documentElement.scrollTop;
var animateScroll = function(timestamp){
if (!currentTime) currentTime = timestamp;
var progress = timestamp - currentTime;
if(progress > duration) progress = duration;
var val = Math.easeInOutQuad(progress, start, final-start, duration);
element.scrollTo(0, val);
if(progress < duration) {
window.requestAnimationFrame(animateScroll);
} else {
cb && cb();
}
};
window.requestAnimationFrame(animateScroll);
};
/*
Focus utility classes
*/
//Move focus to an element
Util.moveFocus = function (element) {
if( !element ) element = document.getElementsByTagName("body")[0];
element.focus();
if (document.activeElement !== element) {
element.setAttribute('tabindex','-1');
element.focus();
}
};
/*
Misc
*/
Util.getIndexInArray = function(array, el) {
return Array.prototype.indexOf.call(array, el);
};
Util.cssSupports = function(property, value) {
if('CSS' in window) {
return CSS.supports(property, value);
} else {
var jsProperty = property.replace(/-([a-z])/g, function (g) { return g[1].toUpperCase();});
return jsProperty in document.body.style;
}
};
// merge a set of user options into plugin defaults
// https://gomakethings.com/vanilla-javascript-version-of-jquery-extend/
Util.extend = function() {
// Variables
var extended = {};
var deep = false;
var i = 0;
var length = arguments.length;
// Check if a deep merge
if ( Object.prototype.toString.call( arguments[0] ) === '[object Boolean]' ) {
deep = arguments[0];
i++;
}
// Merge the object into the extended object
var merge = function (obj) {
for ( var prop in obj ) {
if ( Object.prototype.hasOwnProperty.call( obj, prop ) ) {
// If deep merge and property is an object, merge properties
if ( deep && Object.prototype.toString.call(obj[prop]) === '[object Object]' ) {
extended[prop] = extend( true, extended[prop], obj[prop] );
} else {
extended[prop] = obj[prop];
}
}
}
};
// Loop through each object and conduct a merge
for ( ; i < length; i++ ) {
var obj = arguments[i];
merge(obj);
}
return extended;
};
// Check if Reduced Motion is enabled
Util.osHasReducedMotion = function() {
if(!window.matchMedia) return false;
var matchMediaObj = window.matchMedia('(prefers-reduced-motion: reduce)');
if(matchMediaObj) return matchMediaObj.matches;
return false; // return false if not supported
};
/*
Polyfills
*/
//Closest() method
if (!Element.prototype.matches) {
Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector;
}
if (!Element.prototype.closest) {
Element.prototype.closest = function(s) {
var el = this;
if (!document.documentElement.contains(el)) return null;
do {
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null && el.nodeType === 1);
return null;
};
}
//Custom Event() constructor
if ( typeof window.CustomEvent !== "function" ) {
function CustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
CustomEvent.prototype = window.Event.prototype;
window.CustomEvent = CustomEvent;
}
/*
Animation curves
*/
Math.easeInOutQuad = function (t, b, c, d) {
t /= d/2;
if (t < 1) return c/2*t*t + b;
t--;
return -c/2 * (t*(t-2) - 1) + b;
};
/* JS Utility Classes */
(function() {
// make focus ring visible only for keyboard navigation (i.e., tab key)
var focusTab = document.getElementsByClassName('js-tab-focus');
function detectClick() {
if(focusTab.length > 0) {
resetFocusTabs(false);
window.addEventListener('keydown', detectTab);
}
window.removeEventListener('mousedown', detectClick);
};
function detectTab(event) {
if(event.keyCode !== 9) return;
resetFocusTabs(true);
window.removeEventListener('keydown', detectTab);
window.addEventListener('mousedown', detectClick);
};
function resetFocusTabs(bool) {
var outlineStyle = bool ? '' : 'none';
for(var i = 0; i < focusTab.length; i++) {
focusTab[i].style.setProperty('outline', outlineStyle);
}
};
window.addEventListener('mousedown', detectClick);
}());
// Tutorial - https://codyhouse.co/tutorials/how-stacking-cards
(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);
};
};
}
}());
[site-socialshare]