Skip to content
WANCRAFT

H.1223D & Experimental

Depth Gallery

Cards on a loose spiral at increasing depth that fly toward the viewer as you scroll — a perspective tunnel.

NewReactMotionCSSTailwindadvanced

Overview

Depth Gallery places cards on a golden-angle spiral at increasing depth and moves a camera through them as the page — or any scrollable container passed as a ref — scrolls. A tall track supplies the scroll distance while a sticky viewport holds the 3D stage; Motion's useScroll reports progress and every card's transform and opacity are written straight to the DOM, so scrolling never re-renders React. The camera pans part of the way toward the focused card, cards fade out as they fly past the viewer and fade into the fog far away, and only the focused card is interactive. Previous/next buttons scroll exactly to the neighbouring card's depth, a counter shows the current title and a polite live region announces it. With reduced motion the tunnel becomes a plain vertical list.

01Installation

Method 01 · Registry

CLI verified

Uses the shadcn CLI in a project initialised with shadcn init. Installs the file to components/ui/depth-gallery.tsx and adds its dependencies.

npx shadcn@latest add https://wancraft.my/r/depth-gallery.json

Method 02 · Copy and paste

  1. 1. Install the dependencies below.
  2. 2. Copy the source from the Code tab into components/ui/depth-gallery.tsx.
  3. 3. Import it and adjust props.
npm install motion
Dependencies
motion
Peer
react ≥ 18, tailwindcss ≥ 4
Added
13 Oct 2026
Updated
—

02Usage

The default configuration. Change properties above to generate a tailored example in the Usage tab.

example.tsx
import { DepthGallery, type DepthGalleryItem } from "@/components/ui/depth-gallery";

export function DepthGalleryExample() {
  const items: DepthGalleryItem[] = ["Dune", "Kiln", "Tide", "Fern", "Ash", "Moss"].map((title, i) => ({
    id: title.toLowerCase(),
    label: title,
    content: (
      <figure className="flex h-full flex-col p-4">
        <div className="flex-1 rounded-lg" style={{ background: `hsl(${i * 50} 70% 60%)` }} />
        <figcaption className="mt-3 text-sm font-medium">{title}</figcaption>
      </figure>
    ),
  }));

  return (
    <DepthGallery items={items} label="Works" />
  );
}

03Props

PropTypeDefaultDescription
items{ id: string; content: ReactNode; label: string }[]—Cards from nearest to farthest; label names the card and is announced.
containerRefObject<HTMLElement | null>—Scrollable ancestor that drives the gallery (give it position: relative so offsets measure correctly). Defaults to the window.
labelstring"Gallery"Accessible name of the gallery.
cardWidthnumber240Card width in px.
cardHeightnumber1.25 × cardWidthCard height in px.
depthGapnumber420Distance between neighbouring cards along Z, in px.
spreadnumber220Radius of the golden-angle spiral in px; 0 lines the cards up.
perspectivenumber900CSS perspective in px.
scrollPerItemnumber360Scroll distance between two cards, in px. Track height = viewport + (n − 1) × scrollPerItem.
showControlsbooleantruePrevious/next buttons and the counter, pinned to the bottom of the viewport.
accentstring"#FF7448"Focused-card edge and focus outline colour.

04Features

  • 01Scroll-linked perspective tunnel driven by Motion's useScroll, with an optional container ref
  • 02Golden-angle spiral layout with a camera that pans toward the focused card
  • 03Fades past the viewer and into the fog; hidden cards get visibility: hidden
  • 04DOM-written transforms: scrolling never re-renders React (state changes only when the focused card changes)
  • 05Previous/next buttons scroll exactly to the neighbouring card's depth
  • 06Deterministic first frame, so server and client markup match
motion · scroll-linkedinput · scrollinput · clickinput · keyboard

05Accessibility

  • A <section> with aria-roledescription="carousel"; cards are list items named "3 of 8: title".
  • Only the focused card is interactive; the others are inert and aria-hidden.
  • Labelled previous/next buttons with visible focus outlines; they stay focusable at the ends (aria-disabled).
  • A polite live region announces "Item 3 of 8: title" as the focused card changes.
  • prefers-reduced-motion renders a plain vertical list where every card is reachable, and buttons scroll instantly.

06Limitations

  • The track adds (n − 1) × scrollPerItem of scroll height; place it where a long scroll section makes sense.
  • Card contents render inside a 3D-transformed layer; very large cards can soften text on some GPUs.