Version selector

This demo has several versions:

  1. CSS scroll-timeline
  2. JavaScript WAAPI + ScrollTimeline
🏠

About this demo

In this demo the cards at the top stack onto each other. When a card is stuck, it scales down so that the following card stacks on top of it.

Original demo by CodeHouse: https://codyhouse.co/tutorials/how-stacking-cards

The Code

const $cardsWrapper = document.querySelector('#cards');
const $cards = document.querySelectorAll('.card__content');

// Pass the number of cards to the CSS because it needs it to add some extra padding.
// Without this extra padding, the last card won’t move with the group but slide over it.
const numCards = $cards.length;
$cardsWrapper.style.setProperty('--numcards', numCards);

// Each card should only shrink when it’s at the top.
// We can’t use exit on the els for this (as they are sticky)
// but can track $cardsWrapper instead.
const viewTimeline = new ViewTimeline({
	subject: $cardsWrapper,
	axis: 'block',
});

$cards.forEach(($card, index0) => {
	const index = index0 + 1;
	const reverseIndex = numCards - index0;
	const reverseIndex0 = numCards - index;

	// Scroll-Linked Animation
	$card.animate(
		{
			// Earlier cards shrink more than later cards
			transform: [ `scale(1)`, `scale(${1 - (0.1 * reverseIndex0)}`],
		},
		{
			timeline: viewTimeline,
			fill: 'forwards',
			rangeStart: `exit-crossing ${CSS.percent(index0 / numCards * 100)}`,
			rangeEnd: `exit-crossing ${CSS.percent(index / numCards * 100)}`,
		}
	);
});

Explanation

To keep the cards stuck, position: sticky is used. Key for this part, is that this stickyness is not applied on the cards themselves (.card) but on the inner .card__content.

As for the animation, the key part is that the wrapping element #cards is being tracked on the entry-crossing range.

The content of each card (.card__content) are animated and are assigned to a part of that range. As this demo contains 4 cards, each card should animate over 25% of the range. This is calculated using the number of cards and the index of each card.

By animating the .card__content elements and not the .card elements, the height of the #cards wrapper – and thus the available scroll estate – is not affected.

⚠️ Your browser does not support Scroll-driven Animations. Please use Chrome 115 or newer.

Stacking Cards

👇 Scroll down to see the effect.