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.
@keyframeui
registry/demos/sticky-deck-case-studies-demo.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>
/ {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'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"
/>
);
}
Live preview, examples and props: https://keyframeui.com/components/sticky-deck#sticky-deck-case-studies-demo




















