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

.card {
	--index0: calc(var(--index) - 1); /* 0-based index */
	--reverse-index: calc(var(--numcards) - var(--index0)); /* reverse index */
	--reverse-index0: calc(var(--reverse-index) - 1); /* 0-based reverse index */
}

@keyframes scale {
	to {
		transform:
			scale(calc(1.1 - calc(0.1 * var(--reverse-index))));
	}
}

#cards {
	--numcards: 4;
	view-timeline-name: --cards-element-scrolls-in-body;
}

.card__content {
	--start-range: calc(var(--index0) / var(--numcards) * 100%);
	--end-range: calc((var(--index)) / var(--numcards) * 100%);

	animation: linear scale forwards;
	animation-timeline: --cards-element-scrolls-in-body;
	animation-range: exit-crossing var(--start-range) exit-crossing var(--end-range);
}

Explanation

To keep the cards stuck, position: sticky is used. Key for this part, is that this stickiness 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 --numcards and --index custom properties.

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.