# Sticky Deck: 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/sticky-deck-case-studies-demo).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/sticky-deck-case-studies-demo
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/keyframeui/sticky-deck-case-studies-demo-light-19d7f262d2623ff2.png
- Registry item: https://keyframeui.com/r/sticky-deck-case-studies-demo.json
- Categories: scroll, cards, sticky
- Listed: 2026-10-15, source last changed 2026-10-15

## Install

```bash
npx shadcn@latest add https://keyframeui.com/r/sticky-deck-case-studies-demo.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/sticky-deck.json`

## Notes from the registry

Live preview, examples and props: https://keyframeui.com/components/sticky-deck#sticky-deck-case-studies-demo

## Files

### `registry/demos/sticky-deck-case-studies-demo.tsx`

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, easeOut, motion, useReducedMotion, useTransform } from "motion/react";

import { StickyDeck, StickyDeckCard, useStickyDeckCard } from "@/components/ui/sticky-deck";

const projects = [
  {
    client: "Lumen Roasters",
    headline: "A coffee brand that sells out every Tuesday",
    services: "Identity · Shop",
    year: 2026,
    stat: "+62% repeat orders",
    photo: "1511920170033-f8396924c348",
    alt: "An espresso being pulled into a small glass",
  },
  {
    client: "Atlas Run",
    headline: "The launch site for a carbon racer",
    services: "Launch · 3D",
    year: 2025,
    stat: "1.2M visits in launch week",
    photo: "1542291026-7eec264c27ff",
    alt: "A red running shoe on a red background",
  },
  {
    client: "Kōen Travel",
    headline: "Booking a Kyoto ryokan in three taps",
    services: "Product · iOS",
    year: 2025,
    stat: "4.9 on the App Store",
    photo: "1545569341-9eb8b30979d9",
    alt: "A pagoda above the rooftops of Kyoto at dusk",
  },
  {
    client: "Moss & Velvet",
    headline: "Furniture you can sit in before it ships",
    services: "Commerce · AR",
    year: 2024,
    stat: "31% fewer returns",
    photo: "1555041469-a586c61ea9bc",
    alt: "A green velvet sofa on a pale wooden floor",
  },
  {
    client: "Prism Labs",
    headline: "A brand system that bends with the light",
    services: "Identity · Motion",
    year: 2024,
    stat: "212 brand assets",
    photo: "1557672172-298e090bd0f1",
    alt: "Swirls of liquid marble in blue and pink",
  },
];

const photo = (id: string) => `https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=1400&h=900&q=80`;

export function StickyDeckCaseStudiesDemo() {
  const scrollRef = React.useRef<HTMLDivElement>(null);
  const [active, setActive] = React.useState(0);

  return (
    <div className="relative w-full overflow-hidden rounded-xl border border-zinc-800 bg-zinc-950 text-white">
      <div ref={scrollRef} className="relative h-[var(--demo-height,36rem)] overflow-y-auto overscroll-contain">
        <div className="sticky top-0 z-10 flex items-center justify-between bg-linear-to-b from-zinc-950 from-60% to-transparent px-5 pt-4 pb-6 font-mono text-[11px] tracking-widest text-white/50 uppercase sm:px-8">
          <span>Selected work</span>
          <span className="flex tabular-nums">
            <span className="relative inline-flex h-4 w-[2ch] overflow-hidden">
              <AnimatePresence initial={false} mode="popLayout">
                <motion.span
                  key={active}
                  initial={{ y: "100%", opacity: 0 }}
                  animate={{ y: 0, opacity: 1 }}
                  exit={{ y: "-100%", opacity: 0 }}
                  transition={{ type: "spring", visualDuration: 0.35, bounce: 0 }}
                  className="text-white"
                >
                  {String(active + 1).padStart(2, "0")}
                </motion.span>
              </AnimatePresence>
            </span>
            &nbsp;/ {String(projects.length).padStart(2, "0")}
          </span>
        </div>

        <div className="mx-auto max-w-5xl px-4 sm:px-8">
          <header className="pt-10 pb-14 sm:pt-16">
            <h2 className="max-w-lg text-4xl/[1.05] font-semibold tracking-tight text-balance sm:text-5xl/[1.05]">
              Five launches, start to finish
            </h2>
            <p className="mt-4 max-w-sm text-sm/6 text-pretty text-white/55">
              Brand, product and launch work for teams who ship. Tap a photo&apos;s edge to go back to it.
            </p>
          </header>

          <StickyDeck
            containerRef={scrollRef}
            onActiveChange={setActive}
            top={112}
            offsetStep={14}
            scaleTo={0.92}
            dimTo={0.62}
            hold={120}
            className="gap-10"
          >
            {projects.map((project) => (
              <StickyDeckCard
                key={project.client}
                className="h-[min(26rem,calc(var(--demo-height,36rem)-9.5rem))] overflow-hidden rounded-3xl bg-zinc-800 shadow-[0_-16px_40px_-12px_rgb(0_0_0/0.7)] ring-1 ring-white/10"
              >
                <ProjectPhoto src={photo(project.photo)} alt={project.alt} />
                <div className="absolute inset-0 bg-linear-to-t from-black/85 via-black/25 to-black/10" />
                <div className="relative flex h-full flex-col justify-between p-5 sm:p-8">
                  <div className="flex items-center justify-between gap-3 text-xs text-white/75">
                    <span className="rounded-full bg-black/30 px-2.5 py-1 font-medium backdrop-blur-md">
                      {project.client}
                    </span>
                    <span className="font-mono tabular-nums">{project.year}</span>
                  </div>
                  <div className="flex flex-wrap items-end justify-between gap-x-8 gap-y-4">
                    <div className="max-w-md">
                      <p className="font-mono text-[11px] tracking-widest text-white/60 uppercase">
                        {project.services}
                      </p>
                      <h3 className="mt-2 text-2xl/tight font-semibold tracking-tight text-balance sm:text-3xl/tight">
                        {project.headline}
                      </h3>
                    </div>
                    <div className="flex items-center gap-4">
                      <span className="text-sm text-white/70">{project.stat}</span>
                      <a
                        href="#"
                        className="group flex items-center gap-1.5 rounded-full bg-white px-3.5 py-1.5 text-sm font-medium text-zinc-900 transition-colors hover:bg-white/85"
                      >
                        Case study
                        <svg
                          viewBox="0 0 16 16"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="1.75"
                          aria-hidden
                          className="size-3.5 transition-transform duration-200 ease-out group-hover:translate-x-0.5"
                        >
                          <path d="M3 8h10m0 0L9 4m4 4-4 4" strokeLinecap="round" strokeLinejoin="round" />
                        </svg>
                      </a>
                    </div>
                  </div>
                </div>
              </StickyDeckCard>
            ))}
          </StickyDeck>

          <footer className="flex h-[calc(var(--demo-height,36rem)-4rem)] flex-col items-center justify-center text-center">
            <p className="font-mono text-[11px] tracking-widest text-white/50 uppercase">Next up</p>
            <h2 className="mt-3 text-3xl font-semibold tracking-tight">Yours?</h2>
            <a
              href="#"
              className="mt-5 rounded-full border border-white/20 px-4 py-2 text-sm font-medium transition-colors hover:bg-white/10"
            >
              Start a project
            </a>
          </footer>
        </div>
      </div>
    </div>
  );
}

/** The photo settles from a slight zoom as its card travels into place. */
function ProjectPhoto({ src, alt }: { src: string; alt: string }) {
  const { progress } = useStickyDeckCard();
  const reduce = useReducedMotion();
  const scale = useTransform(progress, [0, 1], reduce ? [1, 1] : [1.18, 1], { ease: easeOut });

  return (
    <motion.img
      src={src}
      alt={alt}
      draggable={false}
      style={{ scale }}
      className="absolute inset-0 size-full object-cover"
    />
  );
}
```
