updated
A card stack that builds itself as you scroll: each card sticks to the top of the viewport, shrinks back as the next one arrives over it, and is discarded off the top once four are stacked up, so the pile never grows past four. It is built with scroll-driven CSS animations and about fifteen lines of JavaScript. Scroll the page to see it.
⚠️Heads up! This demo uses a feature that is not supported by your browser. We will show you a video recording instead.
















The thing that took me longest to see is that a stacked cards effect is two separate techniques doing two separate jobs, and almost every explanation of it runs them together.
The stacking is not an animation at all. Every card is position: sticky with top: 0, so as you scroll, each one stops at the top of the viewport and the next card slides up underneath it. That alone gets you a pile of cards, in plain CSS, with no timeline involved and no JavaScript. If all you want is a stack, you can stop reading here.
The scroll-driven animation does the other half: it takes the cards that are already stuck and scales them back so the pile reads as depth, then discards the ones at the bottom once the stack is four deep. That is what stops it turning into a heap of forty cards by the end of the page.
A relatively new feature in modern browsers, scroll-driven animations allows you to animate based on scroll progression instead of time. While the basics are pretty simple to master, making the animation rolling is a bit more complex. You’ll need to stack multiple animations and calculate offsets based on the total wrapper height.
The timeline here is a view timeline rather than a scroll timeline: the wrapper declares view-timeline-name: --cards-scrolling, so progress is measured by how far the wrapper has travelled through the viewport rather than by how far the page has scrolled. Each card then attaches to that timeline and runs over a slice of it.
Working out the slice is where it gets gnarly. Each card’s range is expressed against the exit-crossing phase, and its start and end are computed from the card’s own index, the number of cards allowed in the stack, the card height, and the wrapper’s block size. It is a lot of arithmetic in a custom property, and it is the reason this looks harder than it is: the effect is simple, the bookkeeping is not.
To stack multiple animations, we need to create a wrapper element that will be animated for each animation we want to stack. We also need to take care to disable animations for the last few cards, so that they don’t keep shrinking after we scroll past the limit, leaving 4 stacked cards as the final state, as intended.
While the most of the effect happens in pure CSS, we’re still relying on javascript to properly shift the cards as the previous one is discarded. While this might be doable without javascript, the css calculations are already a bit gnarly, and I didn’t want to complicate things further.
Scaling a card down on its own centre makes it recede, which is fine but flat. The rolling comes from one line: transform-origin: center 200%, which puts the origin well below the card, so the same scale reads as the card tipping away from you over an axis somewhere near your knees. It is a small change and it is most of the character of the effect.
The discard keyframe then takes the card out with a negative margin-top as well as opacity and scale, so the cards underneath close the gap as it leaves rather than waiting for it to finish fading.
Scroll-driven animations are still not everywhere, so the demo above is behind @supports (animation-timeline: view()). Where it is not supported you get a video of the effect and a note saying so, rather than a stack of cards sitting there doing nothing, which is the failure mode you get if you ship this and assume.
This is my first time playing with scroll-driven animations, as these are not yet widely supported, I wanted to get a better understanding. My conclusion is that while simple effects can be achieved with fairly simple css, more complex effects require a bit more work. You’ll need to compute animation ranges, which can get pretty intense depending on the effect you’re aiming for, and deal with browsers quirks, because life as a web engineer wouldn’t be fun without them.