Stacking Cards Scroll Effect with position: sticky
The stacking-cards effect pins each card as the reader scrolls and lets the next slide over it, with the covered cards receding slightly to suggest depth. It is one of the most recognisable scroll patterns and one of the most expensive, because every pinned card holds a composited layer for as long as the section is on screen. This guide covers the construction, the cost and the requirements the pattern usually forgets. It sits under scroll-driven media and gallery effects.
When to use this approach
- A short sequence the reader should move through deliberately β three to five steps, a product story, a set of principles.
- When each step deserves the full viewport β the patternβs strength is that it gives each card undivided attention.
- On a landing page rather than a reference page β the effect consumes several viewports of scrolling, which is a reasonable trade for a narrative and a poor one for something a reader is scanning.
Avoid it for lists of more than about five items, for content the reader may want to compare side by side, and anywhere the section sits between the reader and something they are trying to reach.
Implementation
1. Pin the cards with sticky positioning
<section class="stack">
<article class="card">β¦</article>
<article class="card">β¦</article>
<article class="card">β¦</article>
</section>
.stack { display: grid; gap: 0; }
.card {
position: sticky;
inset-block-start: 6rem;
block-size: 70vh;
border-radius: 1rem;
}
Sticky alone already produces a stack: each card holds at the same offset until the next pushes it away. Everything else in this pattern is depth.
2. Add depth with a view timeline
@supports (animation-timeline: view()) {
.card {
animation: recede linear both;
animation-timeline: view(block);
animation-range: exit 0% exit 100%;
transform-origin: center top;
}
@keyframes recede {
to { scale: 0.92; opacity: 0.55; }
}
}
The exit range is what makes this work: the card recedes only as it begins to leave, so it stays at full presence for the whole time it is the current one.
3. Give the section enough scroll to move through
.stack {
/* one viewport of scroll per card, plus a little settle room */
padding-block-end: 20vh;
}
.card { margin-block-end: 30vh; }
Too little space and cards stack almost instantly; too much and the section becomes the wall described above. One viewport of scroll per card is a reasonable starting point.
4. Budget the layers
Keep the card count low, avoid full-bleed photographs inside every card, and check the layer count in DevTools at a mobile viewport rather than assuming.
5. Keep the section usable
@media (prefers-reduced-motion: reduce) {
.card {
position: static; /* an ordinary stacked list */
animation: none;
scale: 1;
opacity: 1;
block-size: auto;
}
}
Reduced motion should give a plain sequence of cards, not a pinned stack with the animation removed β pinning is itself the motion here.
Verification
Count the composited layers in the Layers panel while the section is on screen, at a mobile viewport with device pixel ratio emulation on. This is the check that matters most for this pattern, and it is the one that gets skipped.
Record a scroll through the section and confirm there are no paint entries. A repaint per frame usually means a card is animating something outside the compositor-safe set β a box shadow or a border radius change is the usual culprit.
Tab through the section from the element before it to the element after it, confirming that every cardβs interactive content is reachable in document order and that focus moving to a covered card scrolls it back into view.
Finally, scroll through the section quickly, as a reader who does not want it would. If getting past the stack takes an uncomfortable amount of scrolling, reduce the per-card distance rather than the effect.
Edge cases and gotchas
overflow: hidden on an ancestor breaks sticky. Any scrollable ancestor between the sticky element and the viewport becomes its containing block, so the card sticks to that container instead of the page. This is the most common reason a stack silently does not pin.
Sticky needs an unconstrained parent height. A parent with a fixed height shorter than the stack clips the sticky range, so cards unpin early. The grid in the example above has no explicit height for exactly this reason.
Animating box-shadow for depth. It repaints every frame. Use a filter: drop-shadow() or a pseudo-element with an animated opacity instead, both of which stay on the compositor.
Border radius on a scaled card. Scaling changes the effective radius, so a card scaled to 0.92 has a visually smaller corner than the one on top of it. Compensating means animating the radius, which repaints β or accepting the difference, which is usually invisible.
Stacks inside stacks. Two nested sticky contexts produce behaviour that is hard to predict and harder to explain to a reader. One level is the practical limit.
Browser-specific notes
Sticky positioning is uniformly supported and behaves consistently across engines for this pattern. The differences are in compositing.
Chromium promotes each animating card to its own layer and holds those textures for the whole section, which is where the memory figures above come from. Its Layers panel is the best tool for checking the total, and it reports a compositing reason per layer so you can confirm each promotion is intentional.
Safari on iOS is the environment where this pattern most often breaks. Under memory pressure it discards layer textures and re-rasterises, which appears as a blank card for a frame or two. Fewer cards and smaller images are the only reliable mitigations; there is no way to ask for a larger budget.
Firefox composites the pattern as expected and has no layer viewer, so verifying the layer count there means inferring it from paint behaviour rather than reading it directly. Checking in Chromium and assuming parity is the pragmatic approach.
Frequently Asked Questions
Why exit rather than cover for the range?
Because the card should stay at full presence while it is the one being read, and only recede as the next one takes over. A cover range starts shrinking the card as soon as it enters, so it is already slightly diminished at the moment the reader arrives at it β the opposite of the intended effect.
Can this be done without JavaScript entirely?
Yes, and the version in this guide is. The pattern predates scroll timelines and was traditionally built with a scroll listener computing each cardβs state; the sticky-plus-view-timeline version needs no script, and the sticky half of it works even where timelines do not, degrading to a plain pinned stack.
How many cards is too many?
Five is a reasonable ceiling on a phone and eight on desktop, driven by layer memory rather than by taste. Beyond that the pattern also stops working as narrative β a reader who has scrolled through eight pinned cards is no longer discovering, they are waiting.
Does the stack hurt Core Web Vitals?
Not directly. The cards are laid out normally and the animation is compositor-only, so neither layout shift nor interaction latency is affected. What it can affect is the largest contentful paint if the first card contains the largest element and its content loads late β the usual media advice applies.
What is a sensible scroll distance per card?
Roughly one viewport per card, adjusted by how much reading each one asks for. A card carrying a single sentence and an illustration can hold for less; one carrying a paragraph the reader is expected to absorb needs more, or they will be pushed past it mid-sentence.
The mistake in both directions is to set the distance from how the effect looks rather than from how long the content takes to read. Scroll through the finished section reading every card properly and adjust from that, not from a fast pass looking at the animation.
Related
- Scroll-snap and scroll timelines together β snapping is the other way to give each step its own moment
- Compositor-Safe Properties & will-change β the layer promotion this pattern depends on and pays for
- Sticky Header & Navigation Transitions β the other major use of sticky positioning on this site
- Vestibular-Safe Motion Design β full-viewport movement is the strongest trigger there is