# Sticky Photo Backdrop With Cards Passing Over

A shadcn/ui block from the [@weinc](https://we.inc/components?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/weinc/sticky-photo-backdrop-with-cards-passing-over).

- Page, with a live preview: https://shadcn.exchange/r/weinc/sticky-photo-backdrop-with-cards-passing-over
- Screenshot: https://shadcn.exchange/shot/weinc/sticky-photo-backdrop-with-cards-passing-over/light.png
- Registry item: https://we.inc/r/sticky-photo-backdrop-with-cards-passing-over.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/weinc/sticky-photo-backdrop-with-cards-passing-over.json
```

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

## Files

### `components/we-inc/sticky-photo-backdrop-with-cards-passing-over.tsx`

```tsx
import { useEffect, useRef } from "react";

const ZOOM = 0.1; // how much the backdrop eases out (scale 1.1 -> 1.0) across the whole scroll
const WALKS = [
  { day: "Day one", title: "Harbour to the lighthouse", text: "A level start along the sea wall, then a gentle climb to the lamp room for the first long view of the bay.", km: "11 km" },
  { day: "Day two", title: "The red cliffs", text: "The wildest stretch: open headland, a lot of sky, and a lunch stop above the cove where seals haul out.", km: "16 km" },
  { day: "Day three", title: "Through the oak combe", text: "The path drops inland into shade, crosses two footbridges and climbs back out to a farm tea room.", km: "13 km" },
  { day: "Day four", title: "Down to Pellow Sands", text: "An easy final morning that ends on a wide beach, with your bags already waiting at the station hotel.", km: "8 km" },
];

export default function StickyPhotoBackdropWithCardsPassingOver() {
  const box = useRef<HTMLDivElement>(null);
  const photo = useRef<HTMLImageElement>(null);

  useEffect(() => {
    const el = box.current;
    if (!el) return;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    let frame = 0;
    let on = true;
    const draw = () => {
      frame = 0;
      el.classList.toggle("w6stk-moved", el.scrollTop > 8);
      if (reduce || !photo.current) return;
      const m = el.scrollHeight - el.clientHeight;
      const p = m ? el.scrollTop / m : 0;
      photo.current.style.transform = `scale(${(1 + ZOOM * (1 - p)).toFixed(4)})`;
    };
    const req = () => {
      if (on && !frame) frame = requestAnimationFrame(draw);
    };
    const io = new IntersectionObserver(([e]) => {
      on = e.isIntersecting;
      if (on) req();
    });
    io.observe(el);
    el.addEventListener("scroll", req, { passive: true });
    req();
    return () => {
      cancelAnimationFrame(frame);
      io.disconnect();
      el.removeEventListener("scroll", req);
    };
  }, []);

  return (
    <div ref={box} tabIndex={0} role="region" aria-label="Fernmouth coast walk, scrollable preview" className="w6stk relative h-[620px] overflow-y-auto overscroll-contain bg-[#0f1b2a] outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-[#e9dcc4]">
      <style>{`
        .w6stk-photo { transform: scale(1.1); transform-origin: 50% 40%; will-change: transform; }
        .w6stk-hint { transition: opacity .5s ease; }
        .w6stk-moved .w6stk-hint { opacity: 0; }
        @media (prefers-reduced-motion: reduce) { .w6stk-photo { transform: none; will-change: auto; } }
      `}</style>
      <div className="sticky top-0 h-[620px] overflow-hidden">
        <img ref={photo} src="https://images.pexels.com/photos/7694094/pexels-photo-7694094.jpeg?auto=compress&cs=tinysrgb&w=1600" alt="A rocky headland with dry grass and dark green trees above a wide grey sea under cloud" width={1600} height={1280} className="w6stk-photo absolute inset-0 h-full w-full object-cover" />
        <div aria-hidden="true" className="absolute inset-0 bg-gradient-to-b from-[#0f1b2a]/80 via-[#0f1b2a]/30 to-[#0f1b2a]/85" />
        <div className="relative px-6 pt-12 sm:px-12">
          <p className="text-xs font-semibold uppercase tracking-[0.26em] text-[#e9dcc4]">Fernmouth Coast Walks</p>
          <h2 className="mt-3 max-w-lg text-4xl font-semibold leading-tight text-white sm:text-5xl">Four days on the south coast path</h2>
          <p className="mt-4 max-w-sm text-base leading-relaxed text-[#dfe6ee]">Self-guided, with bags carried between inns. The cliffs stay put while the days go by.</p>
        </div>
        <span className="w6stk-hint pointer-events-none absolute right-4 top-4 rounded-full bg-white/90 px-3 py-1.5 text-xs font-medium text-[#0f1b2a]" aria-hidden="true">Scroll inside</span>
      </div>
      <ol className="relative -mt-[620px] space-y-5 px-5 pb-16 pt-[360px] sm:ml-auto sm:max-w-md sm:px-10">
        {WALKS.map((w) => (
          <li key={w.title} className="rounded-xl bg-[#f6f1e7] p-6 text-[#13202f] shadow-[0_24px_48px_-20px_rgba(0,0,0,0.6)]">
            <div className="flex items-baseline justify-between text-xs font-semibold uppercase tracking-[0.18em] text-[#5a6675]">
              <span>{w.day}</span>
              <span>{w.km}</span>
            </div>
            <h3 className="mt-2 text-xl font-semibold">{w.title}</h3>
            <p className="mt-2 text-sm leading-relaxed text-[#3c4756]">{w.text}</p>
          </li>
        ))}
        <li>
          <a href="#" className="flex h-12 items-center justify-center rounded-xl bg-[#e9dcc4] text-sm font-semibold text-[#0f1b2a] transition hover:bg-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-[#0f1b2a]">Choose your start date</a>
        </li>
      </ol>
    </div>
  );
}
```
