/
  1. Catalog
  2. Sticky Scroll Cards

component

Sticky Scroll Cards

Scroll-pinned hero where a front card flips to reveal a stack of back cards, dismissed one at a time as you keep scrolling.

Released September 14, 2026
Made by Axyl

Installation

Install the following dependencies:

Install the following registry dependencies:

Copy and paste the following code into your project:

components/sora-ui/catalog/sticky-scroll-cards.tsx
Loading code…

Update the import paths to match your project setup.

File Structure

sticky-scroll-cards.tsx
scroll-trigger-utils.tsx

Usage

Basic

import { StickyScrollCards } from "@/components/sora-ui/catalog/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/catalog/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 wrapper
  • cardsLayer — 3D perspective container
  • front / back — card surfaces
  • frontIcon / backIcon — icon circle
  • badge — front-only pill (e.g. "Start here")
  • title / description — card text
  • track — scroll track (embedded mode only)

Props

PropTypeDefault
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

PropTypeDefault
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

All card headings, icons, badges, and descriptions stay in the document flow and remain accessible to screen readers throughout the sequence.

Credits

Inspired by Codegrid. Reimplemented for GSAP and vanilla JS.

Built by Axyl. A motion-first component registry for React.

Loading preview...