Card Deck
A stack of cards that flips through on scroll, with configurable perspective and scrub damping plus optional mouse-tilt parallax. Reports the active index as it changes so surrounding UI can follow along.
Included with membership
Members install Card Deck with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.
When to use it
A short sequence that should be read one item at a time — a set of case studies, a pricing tier walk-through, a story in beats. Unlike most of the catalogue it takes markup rather than data, so each card is yours to build. It pins the section while the deck flips, so give it the page to itself.
- Give each child the full width and height of the card — they are stacked and sized by the deck, not by their own content.
- Keep the deck to a handful of cards. The section is pinned for the whole flip, so a long deck is a long time with the page held still.
- Use onActiveIndexChange to drive anything outside the deck, like a counter or a changing caption.
Avoid:
- Do not set a height through className. The track height is computed from scrollSpeed, and overriding it desynchronises the flip from the scroll.
- Do not nest it inside another pinned section — two components pinning the same scroll range leaves neither working.
- Do not put a form or a scrollable panel inside a card. The deck owns the scroll while it is pinned.
Props
A stack of full-bleed cards that deals itself as you scroll. The section pins, the top card slides off to the left with a slight rotation, and the next one rises into its place. You supply the cards as children — the component only owns the stacking, the timing and the pin.
<CardDeck />
Wraps a set of cards and flips through them across a pinned scroll section.
| Prop | Type | Default | Description |
|---|---|---|---|
children* | React.ReactNode | — | One element per card, in deal order. Each is placed in its own absolutely-positioned full-size layer, so give the card itself the width, height and padding you want. |
perspective | number | 1200 | Depth of the 3D viewing frustum, in pixels, set on the pinned stage. Lower values exaggerate the tilt and the slide; higher values flatten them. |
scrollSpeed | number | 1.0 | How much scroll distance the section occupies. Track height is (cards × 100 × scrollSpeed + 100) viewport heights, so 2 means every card takes twice as long to deal. |
scrubLerp | number | 1.2 | Seconds the animation takes to catch up to the scroll position. 0 locks the deck to the scrollbar exactly; higher values let it trail and settle. Overridden to 0.3 on the lowest quality tier. |
enableMouseTilt | boolean | true | Tilts the top card up to 10° toward the cursor and lifts it 1.5%. Forced off when the visitor prefers reduced motion, and on the lowest quality tier. |
onActiveIndexChange | (index: number) => void | — | Receives the zero-based index of the card currently on top. Called on every scroll update, not only when the index changes — compare against your own last value if you need an edge. |
className | string | "" | Appended to the outer scroll track. The track's height is computed from scrollSpeed, so avoid setting a height here. |
disableAdaptiveQuality | boolean | false | Keeps the pointer tilt and the full scrub length on every device. By default the runtime drops the tilt and shortens the scrub on the most constrained tier. Set this when the deck is the point of the page and a plainer version of it is worse than a slower one. |
ssr: false if your route renders on the server.