The final step in implementing this code is the JavaScript portion. The JavaScript file is relatively easy to follow, and contains logic for the scroll behavior. I've added comments to the JavaScript code to explain how it works:
document.addEventListener('DOMContentLoaded', () => {
// Grab container that holds the cards
const container = document.querySelector('.stack-cards-container');
// Makes an array of each individual card
const cards = Array.from(document.querySelectorAll('.stack-cards__item'));
// Distance from top of page to top of container
const containerTop = container.offsetTop;
// Viewport height in pixels
const vh = window.innerHeight;
// Total number of cards
const numCards = cards.length;
// How much of the viewport scroll it takes to flip a card:
// Adjust this between 0.2 (quick flip) up to 1 or more (slower flip):
const scrollFactor = 0.5; // 0.5 each card flips every 50% of a viewport scroll
// Converts the factor into a pixel value
const scrollPerCard = vh * scrollFactor;
// Compute how tall the container needs to be
const totalScroll = scrollPerCard * numCards + vh;
// Apply it
container.style.height = `${totalScroll}px`;
// Function runs everytime you scroll
function onScroll() {
const scrollY = window.scrollY;
cards.forEach((card, i) => {
// We want last card in the DOM to flip first,
// so reverse the index:
const rev = numCards - 1 - i;
// Calculate at what scroll position this card should start sliding up
const threshold = containerTop + scrollPerCard * rev;
if (scrollY > threshold) {
// Once you've scrolled past the threshold, add the class to slide it up
card.classList.add('slide-up');
} else {
// Otherwise, make sure it is back in place
card.classList.remove('slide-up');
}
});
}
// Run once on load in case the page is already scrolled
onScroll();
// Re-run whenever the user scrolls
window.addEventListener('scroll', onScroll);
});
The second step is the most involved process: implementing the CSS for the cards. There's a lot of moving parts in this code.
A lot of the root variables determine the sizing of different areas of the cards:
:root {
--font-size-h-sm: 1.3rem;
--card-width: calc(var(--font-size-h-sm) * 25);
--gap-x: 12px;
--gap-y: 18px;
--step: 1;
/* total width of all cards laid out with gaps */
--num-cards: 6;
--deck-width: calc(
var(--card-width)
+ (var(--num-cards) - 1) * var(--gap-x)
);
--half-deck: calc(var(--deck-width) / 2);
}
Additionally, there is a lot of styling for the card elements for the desk top version:
.stack-cards-container {
position: relative;
}
.stack-cards {
position: sticky;
top: 30%;
transform: translateY(-30%);
width: 100%;
height: 0;
}
.stack-cards__item {
position: absolute;
/* vertical centering + your stack offsets via nth-child top */
top: 30%;
transform: translateY(-30%);
height: 250px;
width: var(--card-width);
color: white;
will-change: opacity, transform;
transition:
transform 1.3s cubic-bezier(0.9,-0.2,0.1,1.2),
opacity 1.3s cubic-bezier(1,0,0,1);
}
.stack-cards__item h3 {
margin: 5px 0;
color: #a59cc8;
}
.stack-cards__item p {
text-align: justify;
margin-top: 5px;
}
.stack-cards__item a {
color: #a59cc8;
}
/* Vertical “stack” offsets */
.stack-cards__item:nth-child(1) {
top: calc(var(--gap-y) * (1 - var(--step)));
}
.stack-cards__item:nth-child(2) {
top: calc(var(--gap-y) * (2 - var(--step)));
}
.stack-cards__item:nth-child(3) {
top: calc(var(--gap-y) * (3 - var(--step)));
}
.stack-cards__item:nth-child(4) {
top: calc(var(--gap-y) * (4 - var(--step)));
}
.stack-cards__item:nth-child(5) {
top: calc(var(--gap-y) * (5 - var(--step)));
}
.stack-cards__item:nth-child(6) {
top: calc(var(--gap-y) * (6 - var(--step)));
}
/* Horizontal centering of the fanned deck */
.stack-cards__item:nth-child(1) {
left: calc(50% - var(--half-deck) + 0 * var(--gap-x));
z-index: 3;
}
.stack-cards__item:nth-child(2) {
left: calc(50% - var(--half-deck) + 1 * var(--gap-x));
z-index: 3;
}
.stack-cards__item:nth-child(3) {
left: calc(50% - var(--half-deck) + 2 * var(--gap-x));
z-index: 3;
}
.stack-cards__item:nth-child(4) {
left: calc(50% - var(--half-deck) + 3 * var(--gap-x));
z-index: 3;
}
.stack-cards__item:nth-child(5) {
left: calc(50% - var(--half-deck) + 4 * var(--gap-x));
z-index: 3;
}
.stack-cards__item:nth-child(6) {
left: calc(50% - var(--half-deck) + 5 * var(--gap-x));
z-index: 3;
}
.inner {
position: relative;
z-index: 2;
background: #2c2a32;
border-radius: 1.5rem;
border: 1px solid black;
width: 100%;
height: 100%;
padding: calc(var(--card-width) * .1);
}
.inner-scroller {
overflow-y: auto;
height: 100%;
overscroll-behavior: contain;
-webkit-overflow-scrolling: touch;
touch-action: pan-y;
}
.stack-cards__item h3 {
margin-top: 0;
font-weight: 500;
}
.stack-cards__item p {
font-weight: 300;
}
.stack-cards__item .counter {
position: absolute;
right: 0px;
bottom: 0px;
width: 50px;
height: 50px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
letter-spacing: .1em;
color: #a59cc8;
text-align: center;
}
/* Slide-up animation: keep X-center, move Y off-screen and fade */
.stack-cards__item.slide-up {
transform: translateX(calc(-50% + var(--half-deck) - var(--half-deck)))
translateY(-100vh);
opacity: 0;
transition:
transform 1.3s cubic-bezier(0.9,-0.2,0.1,1.2),
opacity 0.8s ease-out;
}
However, making the code responsive for mobile only required a couple tweaks!:
/* on really small screens, stop fanning and just center each card full-width */
@media (max-width: 800px) {
.stack-cards__item {
width: 70vw;
height: 60vh;
left: 35%!important;
transform: translate(-50%, -60%) !important;
}
/* stack them vertically with a little gap */
.stack-cards__item:nth-child(n) {
top: calc( (var(--gap-y) * ( (var(--num-cards) - 1) )) + 10vh + ( (var(--gap-y)) * ( (var(--num-cards) - 1) )) );
}
}
This tutorial consists of three steps. The easiest step is adding the cards into the body content of your HTML document. They are relatively simple to build, with few elements required. The important thing to keep in mind is that the bottom card in your HTML will actually appear as your top card in the stack on the web page:
<div class="stack-cards-container">
<div class="stack-cards">
<div class="stack-cards__item">
<div class="inner">
<div class="inner-scroller">
<h3>Example Card 2</h3>
<p>Enter Content Here.</p>
</div>
<div class="counter">2/2</div>
</div>
</div>
<div class="stack-cards__item">
<div class="inner">
<div class="inner-scroller">
<h3>Example Card 1</h3>
<p>Enter Content Here.</p>
</div>
<div class="counter">1/2</div>
</div>
</div>
</div>
This demo showcases another way cards can be utilized to display information. The stacking keeps your content compact, while the scroll effects are engaging for users to interact with.
This effect showcases 6 cards stacked on top of themselves. Scrolling down removes cards from the stack, while scrolling up adds cards back onto the stack. On desktop, you can visually see the cards stacked on each other, with a cool bouncing effect with each scroll. On smaller screens, the stacking and bouncing effects are removed to optimize for reduced screen widths.
The "Sticky Stacking on Scroll" is an effect created by Misala. I've built upon this concept and optimized it for mobile.
I found this code documented on the Prismic Blog, for scroll effects. It was the 8th example on the page.
The date of the blog post is March 13, 2025.