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.

Sticky pins, the timeline scales Each card is sticky at the same offset, so as the reader scrolls each one pins in place while the next arrives beneath it. A view timeline on each card scales and dims it as it is covered, which produces the sense of depth. Sticky supplies the pinning; the timeline supplies everything visual. viewport card 1 β€” scaled 0.92, dimmed card 2 β€” scaled 0.96 card 3 β€” current full scale, full opacity Division of labour position: sticky; top: 6rem pins each card β€” no animation involved animation-timeline: view(block) scales and dims as the card is covered Sticky alone gives a stack with no depth; the timeline alone gives movement with no pinning.

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

Layer cost of a pinned stack Each pinned card that is animating a transform holds its own composited layer for the whole section. Five cards on a desktop is unremarkable. The same five full-width cards on a phone at three times device pixel ratio reserve several times more memory, because texture cost scales with the square of the pixel ratio. approximate texture memory for a section of full-width cards 3 cards, 1x ~3 MB 5 cards, 2x ~20 MB 5 cards, 3x ~45 MB The device with the least GPU memory pays the most, which is why this pattern is the one most likely to produce blank flashes on mid-range phones while looking flawless on a development machine.

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

Pinned does not mean skippable Three requirements. The cards remain in document order regardless of how they stack visually, so tab order follows the reading order. Covered cards must not trap focus behind the one on top. And the section must be passable β€” a reader who does not want the effect should be able to scroll past it at normal speed. Document order is the reading order the visual stack is produced by sticky and transforms, which do not reorder the DOM Covered cards must not trap focus a card scaled and dimmed behind another is still focusable β€” check tabbing through the section The section must be passable a stack that consumes several viewports of scrolling is a wall to a reader who wants the next section All three are independent of the animation, and all three are commonly missing from this pattern.
@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.


Up: Scroll-Driven Media & Gallery Effects