component
Scroll-pinned hero where a front card flips to reveal a stack of back cards, dismissed one at a time as you keep scrolling.
Installation
File Structure
Usage
Basic
import { StickyScrollCards } from "@/components/sora-ui/effects/sticky-scroll-cards";
const front = {
title: "First Frame",
description: "A single moment, held in place before everything begins to move.",
};
const cards = [
{ title: "Final Hold", description: "Everything settles into place." },
{ title: "Layered Time", description: "Moments stack and reveal themselves." },
{ title: "Weight & Flow", description: "Elements ease in and out with balance." },
{ title: "Soft Motion", description: "Subtle shifts build a quiet rhythm." },
];
export default function Section() {
return (
<StickyScrollCards
cards={cards}
front={front}
headline="Scroll to pin, flip, and let go"
/>
);
}Codegrid preset (variant="studio")
Cream canvas, condensed display type, and the original Codegrid "MaximaTherapy" palette cycled across back cards:
<StickyScrollCards
cards={cards}
front={front}
headline="Scroll to pin, flip, and let go"
variant="studio"
/>variant="minimal" (default) uses theme tokens (bg-background/bg-foreground).
Override any slot via the classNames prop.
Custom class map (advanced)
Import STICKY_SCROLL_CARDS_STUDIO_CLASSES to extend the studio preset:
import {
STICKY_SCROLL_CARDS_STUDIO_CLASSES as studio,
StickyScrollCards,
} from "@/components/sora-ui/effects/sticky-scroll-cards";
<StickyScrollCards
cards={cards}
classNames={{ front: cn(studio.front, "shadow-2xl") }}
front={front}
headline={headline}
variant="studio"
/>;Docs preview
embedded = CSS sticky scroll track (no GSAP pin) sized for a preview panel. Pass
scroller when inside a scroll panel.
<StickyScrollCards
cards={cards}
containerQuery
embedded
front={front}
headline={headline}
scroller={viewport}
variant="studio"
/>Timing
Total scroll distance is dismissStart + cards.length × cardDismissDuration (svh).
Back cards dismiss one at a time in reverse order — the last card in the array
leaves first.
<StickyScrollCards
cards={cards}
front={front}
headline={headline}
timing={{
cardsEnterEnd: 500,
cardFlipTrigger: 1000,
dismissStart: 1500,
cardDismissDuration: 500,
flipTiltAngles: [-10, -20, -5, 10],
dismissTiltAngles: [-50, -60, -45, 50],
}}
/>flipTiltAngles/dismissTiltAngles are cycled by index — pass fewer values than
cards.length and they repeat.
Styling
3D flip
The front card flips rotationY: 0 → 180 while back cards flip -180 → 0, so
both live in the same stacking position with backface-visibility: hidden. If
you override classNames.front/classNames.back, keep backface-visibility
intact or the flip will show the reverse face mid-turn.
Card colors
In variant="studio", back card colors come from a cycled palette applied as
inline style, not Tailwind classes — classNames.back layers on top via
className, it doesn't replace the color.
Class slots
className styles the root section. Pass any of these to classNames to
override an inner layer:
headline— hero headline wrappercardsLayer— 3D perspective containerfront/back— card surfacesfrontIcon/backIcon— icon circlebadge— front-only pill (e.g. "Start here")title/description— card texttrack— scroll track (embedded mode only)
Props
| Prop | Type | Default |
|---|---|---|
front? | StickyScrollCardsItem | - |
cards? | StickyScrollCardsItem[] | - |
headline? | string | - |
badgeLabel? | string | "Start here" |
variant? | "minimal" | "studio" | minimal |
embedded? | boolean | false |
scroller? | Element | Window | - |
refreshPriority? | number | -1 |
timing? | StickyScrollCardsTiming | - |
className? | string | - |
classNames? | StickyScrollCardsClassNames | - |
StickyScrollCardsItem
| Prop | Type | Default |
|---|---|---|
title? | string | - |
description? | string | - |
icon? | ReactNode | - |
When to Use
Use StickyScrollCards for a hero moment that needs to hold attention through a
reveal-then-dismiss sequence — feature highlights, a portfolio's opening act, or
any section where a single held frame should fan out into a short, deliberate
series before releasing the scroll. Fewer than 3 back cards makes the dismiss
phase feel abrupt; 3–5 gives the rhythm room to land.
Accessibility
Card content stays in the DOM throughout the sequence. When
prefers-reduced-motion: reduce is set, GSAP skips the animation and the section
renders in its fully-dismissed end state immediately.
Credits
Inspired by Codegrid. Reimplemented for GSAP and vanilla JS.