Sticky Deck

Cards that pin one after another as you scroll. Each new card slides over the last while the deck behind it settles back a layer, scaling down and dimming, so the earlier cards' edges peek out above the front one. Click an edge to bring that card back.

  • Scroll
  • Cards
  • Sticky

Uses motion, clsx, tailwind-merge

Open in ChatGPTOpen in Claude

How it works

From a connected repo to a merged fix in four steps

Scroll through the deck. Click an edge that peeks out above the front card to go back to it.

01Connect

Plug in the tools you already use

Sign in with GitHub and pick the repos. Slack, Linear and your database connect in a click, read-only until you say otherwise.

02Configure

Tell it how your team works

Set how far the agent goes on its own, who reviews what, and where it reports. Plain toggles, no YAML.

03Ship

Watch it open the pull request

It writes the fix, runs your tests and deploys a preview. You get a PR with the reasoning attached.

04Learn

It gets faster every week

Every review teaches it your conventions. Most teams see time to merge drop within the first month.

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/sticky-deck.tsx and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/sticky-deck

Examples

Finished sections built with Sticky Deck. Each one installs whole, with the component included.

Case Studies

A studio portfolio on photo cards, with tighter edges and a deeper dim for a dark page. Each photo settles from a slight zoom as its card lands, driven by `useStickyDeckCard()`, and the counter at the top rolls to the front card's number.

Selected work01 / 05

Five launches, start to finish

Brand, product and launch work for teams who ship. Tap a photo's edge to go back to it.

An espresso being pulled into a small glass
Lumen Roasters2026

Identity · Shop

A coffee brand that sells out every Tuesday

+62% repeat ordersCase study
A red running shoe on a red background
Atlas Run2025

Launch · 3D

The launch site for a carbon racer

1.2M visits in launch weekCase study
A pagoda above the rooftops of Kyoto at dusk
Kōen Travel2025

Product · iOS

Booking a Kyoto ryokan in three taps

4.9 on the App StoreCase study
A green velvet sofa on a pale wooden floor
Moss & Velvet2024

Commerce · AR

Furniture you can sit in before it ships

31% fewer returnsCase study
Swirls of liquid marble in blue and pink
Prism Labs2024

Identity · Motion

A brand system that bends with the light

212 brand assetsCase study

Pricing

Plans dealt in order, each one reading "Everything in Pro, plus" as it lands on the plan it builds on. Taller edges keep every tier's color showing at the top of the deck.

Pricing

Start small. Every plan builds on the last.

Starter

$0per month

For trying it out on a side project.

What you get

  • 1 repository
  • 50 agent runs a month
  • Pull request summaries
  • Community support
Start building

ProMost popular

$24per seat / month

For developers who ship every day.

Everything in Starter, plus

  • Unlimited repositories
  • 1,000 agent runs a month
  • Draft PRs with previews
  • Priority queue
Upgrade to Pro

Team

$48per seat / month

For teams that review code together.

Everything in Pro, plus

  • Shared rules and conventions
  • Review routing by code owner
  • Usage analytics
  • SSO with Google
Start a team trial

Enterprise

Custombilled yearly

For organizations with security reviews.

Everything in Team, plus

  • SAML SSO and SCIM
  • Self-hosted runners
  • Audit log export
  • 99.9% uptime SLA
Talk to sales

Props

children

TypeReact.ReactNode

One StickyDeckCard per card, in order. Give them the same height: a taller card further back would show below the front one.

top

Typenumber | stringDefault96

Where the front card pins, from the top of the scrollport. A number is in px; a string is any CSS length, like "calc(var(--header) + 3rem)". Leave room above it for the peeking edges: depth * offsetStep.

offsetStep

TypenumberDefault16

How far each buried card's top edge peeks out above the card in front of it, in px.

scaleTo

TypenumberDefault0.94

Scale of the card one layer back, from its top edge. Each further layer multiplies it again, so the deck recedes. Not applied with reduced motion.

dimTo

TypenumberDefault0.86

Brightness of the card one layer back, from 0 to 1. Each further layer multiplies it again. 1 turns dimming off.

depth

TypenumberDefault3

How many cards stay visible behind the front one. A card pushed further back slides under its neighbor and fades out.

hold

Typenumber | stringDefault"20vh"

How much more scrolling the finished deck stays pinned for once the last card lands, before the whole deck moves on with the page. A number is in px; a string is any CSS length. 0 lets it go straight away.

containerRef

TypeReact.RefObject<HTMLElement | null>

The scroll container, when the deck scrolls inside an overflow-y-auto box. Leave it out to pin against the window.

onActiveChange

Type(index: number) => void

Called with the front card's index whenever it changes: once the next card covers half of it. Use it for a step list, a counter or a progress bar.

apiRef

TypeReact.Ref<StickyDeckApi>

Receives { scrollTo(index) }, which scrolls the page (smoothly, or instantly with reduced motion) until that card is at the front. For a step list or a table of contents beside the deck.

className

Typestring

Classes for the deck, a flex column. The gap between cards (gap-8 by default) is also how long each card rests at the front before the next one starts covering it.

card.className

Typestring

StickyDeckCard: classes for the card itself: background, rounding, padding and a fixed height. An upward shadow (shadow-[0_-12px_32px_-16px_rgb(0_0_0/0.25)]) falls on the card behind as it slides over. The dimming layer follows the rounding.

card.children

TypeReact.ReactNode

StickyDeckCard: the card's content. Tabbing into a link or button in a hidden card scrolls the deck to it, and clicking a buried card's edge brings it to the front instead of following a link there.

useStickyDeckCard()

Type() => { index, progress, depth }

Call it in a component inside a StickyDeckCard to tie effects to the deck. progress is a MotionValue that runs 0 → 1 as the card travels into place; depth counts the cards covering it, fractionally while one slides over.