Scroll Card Fan — Free GSAP Deck Spread Snippet

Scroll Card Fan · Scroll · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Low-pivot arc
transform-origin below the card creates the fan.
Center-distance spread
Rotation scales with each card's offset from mid.
Messy-pile start
Resting offsets make the deck feel physical.
One-gesture open
All cards tween from position 0 together.
Focal accent
Center card lifts late in the timeline.
Pinned + scrubbed
Fan progress tracks the scrollbar exactly.
Transform-only
Rotation and translation, zero layout cost.
Count-agnostic
Works with any number of cards unchanged.

About this UI Snippet

Scroll Card Fan — Fan a Deck of Cards Open With the Scrollbar

Screenshot of the Scroll Card Fan snippet rendered live

The scroll card fan takes a neat pile of cards and spreads it into a hand-of-cards arc as you scroll — the same gesture as fanning playing cards on a table, driven by the scrollbar and fully reversible. It's a compact way to introduce a set of features, plans, or categories with one satisfying motion. This snippet builds it with GSAP ScrollTrigger (from a CDN) and a single transform-origin trick.

One low pivot point creates the entire arc

Every card is absolutely stacked in the same box with transform-origin: 50% 130% — a pivot *below* the card's bottom edge. Rotating around that shared low point automatically swings each card outward and upward along a circular arc, exactly like cards pinched at one corner. Without the shifted origin you'd have to hand-calculate an x/y/rotation triple per card to fake the arc; with it, rotation alone does 90% of the work.

Spread is computed from distance-to-center

Each card's target rotation is (i − mid) × 16°, where mid is the middle index. The center card stays upright, neighbors lean ±16°, outer cards ±32° — a symmetric fan that works for any odd or even card count without editing the tween code. A small x: spread × 24 widens the fan beyond what pure rotation gives, and y: |spread| × −6 lifts outer cards slightly so their top corners align along the arc.

The deck starts as a believable pile

Before the trigger runs, gsap.set gives each card a resting offset — 1.5° of alternating rotation and 2px of vertical stagger. That messy-pile look matters: fanning from a mathematically perfect stack reads as sterile, while fanning from a slightly uneven pile reads as physical. Because these offsets are set as start values, the scrubbed tween interpolates from pile to fan and back without snapping.

All cards animate at position 0, plus one accent later

Every card's tween is placed at timeline position 0 so the whole fan opens as one gesture rather than card-by-card. Then, at 75% of the timeline, the center card lifts 34px and scales to 1.06 — a focal accent that lands only after the fan is mostly open, giving the sequence a beginning (spread) and an end (highlight) within one scroll.

Pinned, scrubbed, and compositor-only

The stage pins for +=160% with scrub: 0.4, so the fan opens exactly as far as you've scrolled and glides slightly after each wheel tick. Rotation, translation, and scale are all transforms — no layout or paint work — and will-change: transform promotes each card to its own layer up front, avoiding promotion jank mid-scroll.

z-order comes free from source order

Later cards in the DOM paint on top, so the rightmost card naturally overlaps leftward like a real right-handed fan. Reverse the DOM order (or set explicit z-indexes) to flip the overlap direction.

The fanned cards stay fully interactive

Nothing about pinning or scrubbing disables the DOM: the cards remain real elements at every scrub position, so links, hover states, and click handlers work mid-fan or fully open. ScrollTrigger's pin spacer wraps the stage without intercepting pointer events, and because the cards only carry transforms, their hit areas travel exactly with their rendered positions. A common production touch is enabling a lift-on-hover only once the fan is open — gate it by checking tl.progress() > 0.9 in a pointerenter handler, or toggle a class from an onComplete-style position callback on the timeline.

Customizing it

Change the spread angle, card count (the mid math adapts), or make each card a link. Pair the fan with a scroll sticky stack for the section after, a scroll 3d cards alternative entrance, or stacked cards for a hover-driven cousin.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to reverse-engineer the pivot math by staring at the transform values. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why setting transform-origin to 50% 130% on every card is what turns a plain rotation into a convincing fan arc, or why the spread angle is computed from each card's distance to the middle index rather than hardcoded per card. The same assistant can help you optimize it — checking whether the per-card gsap.set calls that establish the resting pile could be batched, or whether a much larger deck (dozens of cards) would need a cheaper approach than tweening every card at timeline position 0. It's also useful for extending the effect: ask it to make each card a clickable link that pauses the fan on hover, add a shuffle-and-redeal animation before the fan opens, or drive the deck's card count and colors from a data array instead of static markup. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a "scroll card fan" effect in plain HTML, CSS, and JavaScript using GSAP and its ScrollTrigger plugin (load both from a CDN, no build step).

Requirements:
- A stack of absolutely-positioned cards inside one container, all sharing the same box, with transform-origin set to a point below the card's bottom edge (e.g. 50% 130%) so rotation alone produces an outward-and-upward fanning arc rather than a flat spin in place.
- Before any scroll animation runs, give each card a small resting offset (a few degrees of alternating rotation and a couple of pixels of vertical stagger via gsap.set) so the closed pile looks like a physical stack of cards rather than a perfectly aligned block.
- Compute a middle index from the card count, then for every card calculate its rotation as a function of its distance from that middle index (so the center card stays upright and cards further from center lean progressively more, symmetrically in both directions), plus a horizontal x offset and a small vertical y lift that also scale with that same distance value.
- All card tweens must start at the same timeline position (position 0) so the entire fan opens as one synchronized gesture, not a staggered card-by-card reveal.
- Add one additional tween, later in the timeline (e.g. at 75% progress), that lifts and slightly scales up only the center card as a focal accent once the fan is mostly open.
- Wire the whole timeline to a pinned, scrubbed ScrollTrigger so the fan's openness tracks the scrollbar exactly and reverses cleanly back into a pile on scroll-up.
- Keep every animated property a transform (rotation, x, y, scale) with no width/height/layout properties involved, and confirm the cards remain fully clickable at every scrub position since nothing in the pin or scrub should disable pointer events.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Add the GSAP CDNsInclude gsap and ScrollTrigger from the CDN panel.
  2. 2
    Paste HTML, CSS, and JSFive cards stack into a slightly messy pile.
  3. 3
    Scroll into the stageIt pins and the deck begins fanning open.
  4. 4
    Finish the scrollThe center card lifts and scales as the focal point.
  5. 5
    Scroll back upThe fan sweeps back into the pile — fully scrubbed.
  6. 6
    Add or restyle cardsThe distance-to-center math adapts to any count.

Real-world uses

Common Use Cases

Feature intros
Fan five value props open, then detail them with sticky scroll features.
Plan pickers
Fan pricing tiers before a pricing card section.
Portfolio decks
Spread project cards; let a scroll gallery pin carry the detail view.
Game and NFT sites
Literal card reveals; add a 3d flip card on click.
Onboarding choices
Fan the paths a user can take, echoing stacked cards.
Section transitions
Open the fan, then hand off to a scroll sticky stack.
Related: Scroll Chapter Sidebar Story
See the Scroll Chapter Sidebar Story for a related scroll pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

Every card shares transform-origin: 50% 130% — a pivot below its bottom edge. Rotating around that low point swings the card outward and upward along a circle, like playing cards pinched at one corner. Without the shifted origin you would need hand-tuned x, y, and rotation per card to fake the same arc.

From its distance to the center index: rotation is (i − mid) × 16°, so the middle card stays upright and outer cards lean the most, symmetrically. A small x shift widens the fan and a negative y proportional to |spread| aligns the top corners along the arc. Change one multiplier to open or tighten the whole fan.

Yes — add or remove .scf-card elements and everything adapts, because mid is computed as (count − 1) / 2 and every tween derives from distance-to-center. For big decks, lower the 16° multiplier (for example 60° total divided by count) so outer cards don't rotate past legibility, and consider raising the pinned end distance.

gsap.set gives each card 1.5° of alternating tilt and a 2px vertical stagger before the trigger runs. A mathematically perfect stack fans open looking sterile; a slightly uneven pile reads as a physical deck. Since these are start values, the scrubbed tween interpolates smoothly from pile to fan and back without any snap.

Fully — pinning and scrubbing never disable the DOM, and transform-only animation means each card's hit area travels with its rendered position. Wrap card contents in anchors or attach click handlers as usual. To add hover lifts only after the fan opens, gate the effect on tl.progress() > 0.9 so a half-open deck doesn't respond like a finished layout.

Render cards from an array, then build the set() offsets and timeline in a mount effect (useEffect, onMounted, or ngAfterViewInit) inside gsap.context scoped to a container ref, reverting it in the cleanup so the pin unregisters on unmount. Compute mid from the array length so adding data adds cards. Card visuals translate directly to Tailwind gradient and shadow utilities.