Scroll✨ Premium

Card Stack

Five 80 px cards in position: sticky, pinned 10 px apart inside a 280 px box: each one enters through a view()-driven animation — opacity 0 → 1, translateY 40 → 0, scale .92 → 1 over 64 px of scrolling — then stays under the next one with its 10 px edge showing. CSS only, zero JavaScript, 720 px of travel.

CSSStackSticky

Updated

This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Scroll has 70 effects, including 12 free. Explore the category →

3 usage examples

Transaction history in a bank statement — each transaction fades in and stays at the edge of the stack — Card Stack example 1

① Transaction history in a bank statement — each transaction fades in and stays at the edge of the stack

WhenPersonal finance app: under the account header (balance, month, filter), the September statement is a list of transactions — label, date, category, amount — that the customer scrolls; each row fades and scales in as it enters, then pins under the next one.
WhyEach transaction arrives on its own — fade and scale over 51 px of scrolling, the next one a 76 px pitch later — then tucks under the one that follows, its 10 px edge showing: the stack under the header grows by one band per row scrolled past, and its height tells the customer where they are in the month, with no counter or pagination; scrolling back brings the rows out one by one, whole. Slide In Cards (fx-0562) would also bring each row in, once from the left, once from the right, then let it leave through the top of the box: nothing accumulates, the entrance alone does not say how many transactions have gone by.
SettingsLight background: white bordered cards (#e2e5ea, 8% shadow) and dark text instead of gradients, 100% width, 64 px tall, 12 px gap (76 px pitch). Six cards instead of five: :nth-child(6) { top: 60px } rule added. 270 px box under a 90 px header, 660 px track (390 travel); animation-range and starting state kept — measured: the sixth row enters between 128 and 178 px and pins at 336. Credits in green #15803d (5:1 on white).
Nutrition report results — one indicator at a time, the values already read stay in sight — Card Stack example 2

② Nutrition report results — one indicator at a time, the values already read stay in sight

WhenHealth app, results screen of a day's report: protein, carbohydrates, fat, fibre, vitamin D — five cards with the measured value, the target and a verdict, revealed one after another while scrolling.
WhyThe 10 px offset keeps each indicator visible at the edge of the stack under the next one: reading is ordered and the values already read stay within sight — Fade Up (fx-0535) would let them leave the frame once read. A wall of five numbers becomes a sequence, with no tabs or pagination.
SettingsMechanics of the sold code untouched (sticky at 10 → 50 px, entry entry 80%, starting state); 84 px cards with a 16 px gap (100 px pitch), one gradient per nutrient (indigo, amber, pink, green, cyan) whose second color is darkened to hold 5:1 with white text, content in two columns (name + target, 21 px value + verdict). 296 px box under a 64 px header, 700 px track (404 travel) — measured: fourth card from 26 to 92 px, fifth from 126 to 192, stack complete at 370.
Site log of a construction project — the week's entries stack in chronological order — Card Stack example 3

③ Site log of a construction project — the week's entries stack in chronological order

WhenConstruction management tool: the week 39 log lists five dated entries — site meeting, concrete delivery, inspection, incident, delivery — that the project manager scrolls through; each card appears in turn and joins the stack.
WhyThe chronological order is imposed by the scroll, one entry after another, without the vertical line or markers of Timeline Scroll (fx-0582): the cards are self-contained, each carries its date and type, and the colored bar on the left is enough to type the event. The log stays a plain list of divs, generated server-side with no timeline structure.
SettingsFlat bordered #1f2937 cards, 4 px left bar colored by type (meeting amber, delivery cyan, inspection green, incident red), 88 px tall, 14 px gap (102 px pitch), entry number on the right. 302 px box under a 58 px header, 720 px track (418 travel); sticky and animation-range unchanged — measured: fourth card from 22 to 92 px, fifth from 124 to 194, pinned at 282 and 374.

How it works

The sold HTML has four levels: the .demo-preview frame (centered flex, min-height: 300px, position: relative, #0a0a0f background, white text, box-sizing: border-box for everything inside), the scrolling box .sdc-scroll-container (100% × 280px, overflow-y: auto, scroll-behavior: smooth, a 4 px scrollbar styled through ::-webkit-scrollbar), the .ss-cardstack-content track (1000 px tall, padding: 20px 12px) and five .ss-cardstack-card: 90% wide, 80 px tall, margin: 0 auto 16px, border-radius: 12px, centered flex, white 13.6 px text at weight 700, 0 4px 20px rgba(0,0,0,.3) shadow, one 135° gradient per card (#4f46e5 → #6366f1 for the first, #ec4899 → #f472b6 for the fifth). The vertical pitch is 96 px: the cards start 20, 116, 212, 308 and 404 px from the top of the track, and the travel is 1000 − 280 = 720 px. Below the box, the span.sdc-scroll-hint “Scrollez ↓” (absolute, 8 px from the bottom of the frame, 10.4 px, white at 30%) pulses between .3 and .7 every 2 s. The JavaScript field is empty; no site CSS variable, no console error.

The entrance is a CSS animation whose clock is the scroll. The base rule sets the starting state — opacity: 0; transform: translateY(40px) scale(.92) — and the @keyframes ssCardStack holds a single to frame (opacity: 1; transform: translateY(0) scale(1)). animation-timeline: view() creates an anonymous view timeline: the scroller is the nearest scrolling ancestor, .sdc-scroll-container, and progress follows the card's position within its 280 px. animation-range: entry entry 80% (short for entry 0% entry 80%) limits the animation to the entry: 0% when the top of the card touches the bottom edge of the box, 100% when the whole card is inside; 80% of an 80 px card is 64 px of scrolling for the whole fade. animation-duration: auto, linear and animation-fill-mode: both do the rest: before the range, the starting state; after it, the final state — in both directions (measured: scrolling back to 0 returns cards 4 and 5 to opacity 0). Measured in Chromium 151, blank page: card 4 goes from 0 to 1 between 28 and 92 px of scrolling (.031 at 30, .5 at 60, .969 at 90), card 5 between 124 and 188 (.563 at 160). The first three are already at 1 at rest: their entry range is over before the travel begins (first FAQ question).

The stacking is plain position: sticky, with a top that grows by 10 px per card (:nth-child(1) at 10 px … :nth-child(5) at 50 px). A card pins when its normal position goes above its top: at 10, 96, 182, 268 and 354 px of scrolling. Later cards, later in the DOM, paint on top: each card leaves 10 px of the previous one showing, and the stack occupies 10 to 130 px of the box. Entry and pinning never overlap — card 4 ends its fade at 92 px and pins at 268. The sticky containing block is the track: its 1000 px keep the five cards pinned to the end, but the stack is complete at 354 px and the remaining 366 px change nothing.

Accessibility

  • prefers-reduced-motion not in the code: measured under emulation, nothing changes — cards 4 and 5 stay at opacity: 0 until you scroll to them, as without the preference, and the hint keeps pulsing. The fade is driven by the reader (no motion without their gesture), which makes it an interaction-triggered animation (WCAG 2.3.3). For a flat fallback, add @media (prefers-reduced-motion: reduce) { .ss-cardstack-card { animation: none; opacity: 1; transform: none } .sdc-scroll-hint { animation: none } }: all five cards are visible from the start and stack without fading — what Firefox already shows.
  • Keyboard: measured, Tab focuses the box without tabindex in Chromium (arrow down 40 px, PageDown 285, End 720) and in Firefox (51, 303, 720); WebKit does not focus a scrolling box without tabindex="0" — add it on .sdc-scroll-container, with an aria-label and a :focus-visible style. On touch screens the gesture must start inside the box; overscroll-behavior: contain keeps the page from scrolling along at the end of the travel.
  • Contrast (white 13.6 px text at weight 700, 4.5:1 threshold): 6.3 → 4.5:1 on the first card, then 4.5 → 3.0 (second), 4.2 → 2.7 (third), 3.5 → 2.5 (fourth) and 3.5 → 2.7 (fifth): four cards out of five fall below the threshold on their light half. Darken the second color of each gradient (#4338ca, #6d28d9, #a21caf, #be185d hold 6:1 and more with white) or put the text on a solid background. The hint at 30% is 2.6:1: decorative, aria-hidden="true".
  • Screen readers and covered content: the five cards are divs read in DOM order; opacity: 0 removes nothing from the accessibility tree, so a screen reader reads all five at once. If the cards carry information, use ul/li or article with a heading. Visually, a pinned card is only readable on its 10 px edge: only scrolling back uncovers it (the sticky undoes itself the other way, measured) — plan for it if your cards are data rather than steps. The “Scrollez ↓” hint is hard-coded in French.

Browser compatibility

CSS only, zero dependencies, zero JavaScript: animation-timeline: view(), animation-range and animation-duration: auto (scroll-driven animations), position: sticky, overflow-y, gradients, @keyframes. Scroll-driven animations exist in Chrome and Edge since version 115, in Safari and iOS since version 26 (measured in WebKit 26.5: same values as Chromium); no version of Firefox supports them. Measured in Firefox 153: animation-timeline and animation-duration: auto are dropped, the computed duration falls to 0 s and the animation, with fill-mode: both, lands on its to frame at load — all five cards are visible at full opacity, with no fade or slide, and stack exactly as elsewhere (10, 20, 30, 40, 50 px at the end of the travel). Nothing is lost, only the entrance is missing. The code has no @supports block: the final state serves as the fallback.

Chrome 115+✓ Full
Edge 115+✓ Full
Safari 26+✓ Full (measured in WebKit 26.5)
Firefox✓ No version — cards visible from the start, stacking without fade (measured in Firefox 153)
iOS Safari 26+✓ Full (touch scrolling of the box)
Android Chrome 115+✓ Full

Where animation-timeline is missing (Firefox, Chrome before 115, Safari before 26): flat cards, opacity 1, no translation or scale, sticky stacking identical. A browser without position: sticky would scroll the cards like an ordinary list. Without the .demo-preview frame, the “Scrollez ↓” hint loses its anchor and falls to the bottom of the page: remove the span or give your own frame position: relative.

The code

HTML structure to paste into your page (the CSS + JS come with Premium):

index.html — structure
<div class="demo-preview">
  <div class="sdc-scroll-container">
    <div class="ss-cardstack-content">
      <div class="ss-cardstack-card">…</div>
      <div class="ss-cardstack-card">…</div>
      <div class="ss-cardstack-card">…</div>
      <div class="ss-cardstack-card">…</div>
      <div class="ss-cardstack-card">…</div>
    </div>
  </div>
  <span class="sdc-scroll-hint">…</span>
</div>
Unlock the full code — €9.90/mo excl. tax

The full HTML + CSS + JS of Card Stack, ready to paste — plus all 811 effects. Or €299 once, lifetime access.

Customize

Options passed to the API or data-* attributes:

Option / propertyDefaultEffect
Number of cards and offset (HTML + CSS — :nth-child(n) { top }) 5 cards, top 10 → 50 px in 10 px steps The step is the visible edge of each card under the next one. A sixth card has neither its own background nor its own top (it takes top: 10px and a transparent background): add :nth-child(6) { top: 60px; background: … }. A 6 px step tightens the stack, 16 px lets the start of a title show on each edge.
Entry range (CSS — animation-range: entry entry 80%) entry 0% → entry 80% (64 px of scrolling for an 80 px card) entry entry 100% stretches the fade over the whole entry (80 px); entry 20% entry 100% delays the start; cover 0% cover 40% keeps going after the card has fully entered. The range reads as a percentage of the card's height: the taller the card, the longer the fade. The linear curve sticks to the gesture; ease-out makes the card appear fast and settle slowly.
Starting state (CSS — base rule opacity: 0; transform: translateY(40px) scale(.92)) invisible, 40 px lower, 92% The @keyframes only holds the to frame: the start is the base rule's, and that is where you set it. translateY(16px) scale(.98) gives a discreet entrance; opacity: .4 lets the card show through before it enters.
Track and box height (CSS — .ss-cardstack-content { height: 1000px }, .sdc-scroll-container { height: 280px }) 1000 px / 280 px (720 travel, stack complete at 354) The travel is track − box; the stack is complete at 354 px (last card pinned), beyond that nothing changes. A 640 px track (360 travel) ends right after; if the number of cards varies, switch to height: auto with a padding-bottom equal to the box height. The 280 px box decides how many cards are already visible at rest (three here).
Card size and gap (CSS — height: 80px, width: 90%, margin: 0 auto 16px) 80 px, 90%, 16 px (96 px pitch) The pitch sets the rhythm of the pinning: one card every 96 px of scrolling. The cards are centered flex boxes with a single text; for a date / label / amount row, switch to justify-content: space-between and a horizontal padding — the sticky and the animation do not depend on the content.
Colors (CSS — :nth-child(n) { background: linear-gradient(135deg, …) }) 5 gradients from indigo to pink, white text Each card carries its own background; the stack is read through these 10 px bands, so keep the colors distinct from one card to the next. The white 13.6 px text only holds 4.5:1 on the first card: darken the second color or switch to solid cards (white bordered on a light background, as in the first scene).
Smooth scrolling and hint (CSS — scroll-behavior: smooth, .sdc-scroll-hint) smooth / “Scrollez ↓” pulsing 2 s smooth only acts on positions set by script (measured: scrollTop = 720 reads back 0 and arrives in about 420 ms in Chromium, 540 in Firefox, 200 in WebKit) — auto if a button drives the box. The hint is a leftover from the catalog: hard-coded French, anchored to the .demo-preview frame; to do without it, remove the span and the @keyframes sdcHintPulse.

FAQ

Because their entry range lies behind the starting point. entry runs from the moment the top of the card touches the bottom edge of the box to the moment the whole card is inside; at rest, cards 1 and 2 (20 → 100 and 116 → 196 px) are entirely within the 280 px of the box, and the third (212 → 292) has only 12 px outside: its entry 0% → 80% range goes from −68 to −4 px of scrolling, already past, and fill-mode: both applies the final frame to it. Measured: at rest, opacity 1 on the three, 0 on cards 4 and 5, which enter between 28 and 92 px then 124 and 188. To have every card fade in, push them below the bottom edge — padding-top: 280px on the track, the travel then starts with an empty screen — or shrink the box below the pitch: 90 px show only the first card.
Frankly: it is the same technique with different settings, not another effect — same position: sticky, same view() with entry entry 80%, same starting state (40 px lower, 92%), same 10 px step, zero JavaScript on both sides. The differences can be read in the two codes: fx-0568 sets 90 px cards at 85% (80 px at 90% here), pins them from 30 to 70 px (10 to 50 here), replaces the fixed 1000 px track with two spacers — 40 px before the first card, 120 px after the last — and gives its box height: 100% and a 20 px padding, instead of a 280 px box whose padding sits on the track. Measured in Chromium, both in a 280 px box: fx-0568 scrolls over 450 px, shows two cards at rest (the third at 11%), pins at 10, 106, 202, 298 and 394 px and its stack occupies 50 → 180 px, the box padding adding to each card's top; Card Stack scrolls over 720 px, shows three cards, pins at 10, 96, 182, 268 and 354 px, stack from 10 to 130 px. Rule of use: start from Card Stack, and take from fx-0568 only its spacers if the number of cards varies — the track then follows the content, provided the final spacer fills what is left of the box under the last pinned card: 100 px in a 280 px box (fx-0568's 120 px are enough), 220 in a 400 px box — measured, with 120 px the last card stops at 154 px and never pins. Height, width and starting top are values to change, not a reason to pick one over the other.
Yes, and the code barely changes: view() takes the nearest scrolling ancestor, so the document if you remove overflow-y: auto from the box (or the box itself), and position: sticky then pins relative to the viewport. Replace the 1000 px track with your sections, with enough content after the last card for it to have time to pin (its normal position minus its top). The 10 px step gets small on a large screen: 24 to 32 px let the title of each covered card show. A fixed header adds to each card's top, otherwise it covers it; the hint, anchored to the frame, would move to position: fixed. Firefox sees the same stack there, without the fade.