Card Stack Scroll
A scroll effect where cards stick to the top one after another, stacking up and shrinking away layer by layer.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add card-stack-scroll.enCard Stack Scroll is a React sticky stacking cards component built with CSS position: sticky and Motion's useScroll. As you scroll, each card pins to the top and stacks over the previous one, and covered cards gradually scale down into the background.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<CardStackScroll/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- Walking through three to five core features on a product landing page, one per card
- Portfolio or case study pages with one project per card
- "How it works" steps that readers scroll through one at a time
- Setting up use cases before a pricing comparison
- Not a good fit: when users should swipe between cards independently of page scroll, use Stacked Peek Carousel
Installation
npx webberui@latest add https://webberui.com/r/card-stack-scroll.en.jsonUsage
Each direct child becomes one card in the stack:
import { CardStackScroll } from "@/components/ui/card-stack-scroll";
<CardStackScroll offsetTop={96}>
<div className="h-80 rounded-xl border bg-white p-8">First</div>
<div className="h-80 rounded-xl border bg-white p-8">Second</div>
<div className="h-80 rounded-xl border bg-white p-8">Third</div>
</CardStackScroll>Examples
With a fixed navigation bar
With a 64px fixed header, set offsetTop a little higher than the header so cards don't pin underneath it. Cards overlap, so give them an opaque background.
import { CardStackScroll } from "@/components/ui/card-stack-scroll";
const features = [
{ title: "Automatic backups", body: "Hourly backups, kept for 30 days." },
{ title: "Team permissions", body: "View, edit and admin access by role." },
{ title: "Audit log", body: "Every action recorded with time and user." },
];
export function FeatureStack() {
return (
<CardStackScroll
offsetTop={88}
peek={16}
cardClassName="h-[420px] rounded-2xl border bg-white p-10 shadow-lg dark:bg-neutral-900"
>
{features.map((f) => (
<article key={f.title}>
<h3 className="text-2xl font-semibold">{f.title}</h3>
<p className="mt-3 text-neutral-600 dark:text-neutral-400">{f.body}</p>
</article>
))}
</CardStackScroll>
);
}cardClassName is applied to each card's animated layer, which saves repeating the same classes on every child.
Tune the depth of the stack
peek sets how much of each card behind stays visible, and scaleStep sets how much each layer shrinks. The last card never scales, and earlier cards shrink the most.
<CardStackScroll peek={24} scaleStep={0.06}>
<div className="h-96 rounded-xl bg-indigo-600 p-8 text-white">Step 1: Connect a data source</div>
<div className="h-96 rounded-xl bg-indigo-500 p-8 text-white">Step 2: Schedule syncs</div>
<div className="h-96 rounded-xl bg-indigo-400 p-8 text-white">Step 3: Publish the dashboard</div>
</CardStackScroll>Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Each direct child becomes one card |
offsetTop | number | 96 | Position where cards stick to the top (px) |
peek | number | 12 | Downward offset of each layer while stacked (px) |
scaleStep | number | 0.04 | How much each layer shrinks as it stacks in |
container | RefObject<HTMLElement> | — | When scrolling happens inside a nested overflow container, pass that container's ref |
cardClassName | string | — | Applied to the outer wrapper of every card |
How it works
- Driven by
useScroll+useTransform; the animation runs on transforms, so it never triggers layout - When the user has "reduce motion" enabled at the system level, it degrades to a plain vertical list
- The container can be the page itself, or any
overflow-y: autosection (as in the demo above)
FAQ
The cards scroll away instead of sticking. Why?
Positioning relies on position: sticky. If an ancestor between the cards and the scroll container has overflow: hidden or overflow: auto, sticky stops working. Inside a nested scroll area, pass that scroll container's ref to container.
Does it work with the Next.js App Router?
Yes. The source already has "use client", and the children can be content rendered by Server Components, passed in as children. The reduced-motion static version switches in after mount, so the first render matches the server output.
How does it perform?
Scaling only touches transform (cards have will-change-transform), so it does not trigger layout. Scroll progress comes from Motion's useScroll; the component adds no scroll listeners of its own.
What happens with reduced motion enabled?
The cards render as a plain vertical list with gap-6, without pinning or scaling.
What are the dependencies?
motion, clsx, tailwind-merge, and the cn helper in @/lib/utils. Without the CLI, copy the source from the Manual tab.