Media

3D Marquee

A four-state image collection that drifts through staggered lanes on a responsive, perspective-tilted plane.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import { cn } from "@/lib/utils"
import type {
  PerspectiveMarqueeData,
  PerspectiveMarqueeItem,
} from "./perspective-marquee.contract"

const MARQUEE_KEYFRAMES = `@keyframes zy-perspective-marquee-up{from{transform:translateY(0)}to{transform:translateY(-50%)}}
@keyframes zy-perspective-marquee-down{from{transform:translateY(-50%)}to{transform:translateY(0)}}
@media(prefers-reduced-motion:reduce){.zy-perspective-marquee-lane{animation:none!important}}`

const MARQUEE_ROOT_SIZE = "h-[24rem] min-h-72 w-full sm:h-[30rem]"

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/perspective-marquee.json

Prompt

Build a React + TypeScript + Tailwind "PerspectiveMarquee" component using
zod. It renders a data-backed image wall on a tilted 3D plane with CSS
keyframes and no animation runtime.

Contract
- Make one zod schema the source of truth:
  { status: "loading" | "empty" | "error" | "ready";
    items: { id: string; src: string; alt: non-empty string }[] }.
- Export PerspectiveMarqueeProps as the inferred data plus div props, with
  columns?: 2 | 3 | 4 (default 4), duration?: number (default 24, clamp to at
  least 8 seconds), reverse?: boolean (default false), and onRetry?: () => void.
- Add paused?: boolean, defaultPaused?: boolean (default false), and
  onPausedChange?: (paused: boolean) => void. Use the controlled value when
  supplied; otherwise own the state from defaultPaused.
- Spread remaining props and merge className via cn(). The component never
  imports mock data.

Behavior
- Four first-class branches: loading is a 3/4-column skeleton matching the
  image-wall rhythm; empty explains that images can be added; error uses
  role=alert and renders a real retry button only when onRetry exists; ready
  renders the moving plane. Treat ready + [] as empty instead of dividing or
  indexing into an empty list.
- Distribute images through min(columns, items.length) lanes in stable source
  order. Duplicate each lane exactly once, then translate its lane between 0
  and -50% for a seamless loop. Adjacent lanes travel in opposite directions
  and add a small duration stagger; reverse mirrors the plane and swaps every
  travel direction.
- Project the lane grid with perspective plus rotateX/rotateZ, but clip it
  inside the root so narrow screens never create horizontal page overflow.
  The plane and all lanes are deterministic during SSR — no viewport reads,
  random values or mount-only item shuffling.
- A prefers-reduced-motion CSS media rule removes lane animation before
  hydration, motion-reduce utilities remove hover lift, and the redundant
  playback button is hidden. The populated plane remains visible and static.
- A visible, keyboard-focusable Pause gallery / Resume gallery button sits
  above the ready-state stage whenever reduced motion is not already active.
  It exposes aria-pressed, updates the controlled/uncontrolled pause state and
  freezes CSS animation-play-state at the current frame rather than jumping
  lanes back to their starting positions.
- The second loop copy is aria-hidden. Every original img keeps a meaningful
  alt, plus async decoding and eager loading only for the first visible image.

Rendering & styling
- Semantic tokens only: bg-background for the stage, bg-card / bg-muted for
  states and media placeholders, border for edges, text-muted-foreground for
  supporting copy, ring for retry focus. No hardcoded color values.
- Use rounded-lg/xl and shadow-sm token utilities rather than custom radius or
  shadow literals. Two pointer-events-none gradient overlays fade all four
  stage edges without intercepting content.
- Keep the default root responsive (24rem tall, 30rem from sm) and allow the
  consumer to override height with className. Put that sizing recipe in one
  shared root class so loading, empty, error and ready all occupy the exact same
  box and status changes never shift surrounding layout.

Customization levers
- Density: choose 2/3/4 columns; reduce columns for cards, keep four for heroes.
- Pace: duration controls the first lane and the stagger is derived from it;
  larger values make the wall calmer without touching geometry.
- Direction: reverse mirrors the plane and flips lane travel as one coherent
  change rather than manually editing each lane.
- Playback: use defaultPaused for an initially still embed, or paused +
  onPausedChange when a parent toolbar or saved preference owns playback.
- Depth: rotateX, rotateZ, perspective and scale are the four geometry knobs;
  change them together and keep the root clipped.
- Surface treatment: swap bg-background for bg-card, or tune the semantic
  gradient overlays; keep image alt text and the hidden duplicate cycle intact.

Concepts

  • Contract-driven four states — a media feed can be slow, missing or unavailable; status is part of the contract so the marquee never collapses into a broken happy-path shell.
  • Stable lane partition — round-robin distribution depends only on item order and column count, keeping server output and hydrated output identical while balancing lane lengths.
  • Seamless loop clone — each lane repeats one identical cycle and translates by half of its combined track; the clone is hidden from assistive technology so visual continuity does not duplicate meaning.
  • Alternating travel — neighbouring lanes move in opposite directions with slightly different durations, breaking visual lockstep without random numbers or unstable layout.
  • User-owned playback — the built-in focusable control changes animation-play-state, so people can stop a continuous effect at its current frame; controlled props let a host toolbar or saved preference own the same state.
  • Perspective as presentation — perspective and rotations apply to one parent plane; item data and state logic remain ordinary two-dimensional React, so depth can be retuned safely.
  • Reduced-motion still — motion preference freezes the populated plane instead of removing it, preserving the composition and image context with no automatic travel or hover lift.

On This Page