Command Palette

Search for a command to run...

Docs
Scroll Tilted Grid

Scroll Tilted Grid

An editorial two-column image grid where pairs of stills rise from below tipped forward, settle into focus, then tilt away over the top edge as the page scrolls. Optional infinite loop appends more cycles via IntersectionObserver.

Installation

Usage

Full-page (default)

The component ships with sectionPadding="20vh", so the grid breathes on a landing page without any wrapper work — page scroll drives the choreography.

import { ScrollTiltedGrid } from "@/components/ruixen/scroll-tilted-grid";
 
export default function Page() {
  return (
    <main className="relative min-h-screen overflow-x-hidden">
      <section className="flex min-h-screen flex-col items-center justify-center px-6 text-center">
        <h1 className="text-3xl font-medium tracking-tight md:text-5xl">
          A field of stills
        </h1>
      </section>
 
      <ScrollTiltedGrid loop />
    </main>
  );
}

Inside a bounded container

Pass a ref to a fixed-height self-scrolling ancestor via container, and shrink sectionPadding so the grid fits the smaller frame. useScroll and the loop's IntersectionObserver measure against the ref instead of the viewport.

import { useRef } from "react";
import { ScrollTiltedGrid } from "@/components/ruixen/scroll-tilted-grid";
 
export default function Preview() {
  const ref = useRef<HTMLDivElement>(null);
  return (
    <div ref={ref} className="h-[480px] overflow-y-auto rounded-xl">
      <ScrollTiltedGrid
        container={ref}
        loop
        maxCycles={4}
        sectionPadding="1rem"
        maxWidth="md"
        gap={6}
      />
    </div>
  );
}

Custom images

<ScrollTiltedGrid
  images={[
    "/gallery/01.jpg",
    "/gallery/02.jpg",
    "/gallery/03.jpg",
    "/gallery/04.jpg",
  ]}
/>

Props

PropTypeDefaultDescription
imagesreadonly string[]DEFAULT_GRID_IMAGESImage URLs to render.
loopbooleanfalseCycle the source list and append more pairs as the user nears the bottom — a perceptually infinite scroll.
initialCyclesnumber3Initial number of cycles to render when loop is on.
aspectRatiostring"3/4"CSS aspect-ratio value applied to each tile (e.g. "3/4", "2/3").
maxWidth"sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "none""lg"Tailwind max-w-* token controlling the column width.
gap4 | 6 | 8 | 10 | 12 | 1410Tailwind gap-* token between tiles.
perspectivenumber900CSS perspective (px) applied to each tile.
maxTiltnumber70Maximum rotateX magnitude (deg) at the entry and exit poses. Symmetric — entry tilts forward +maxTilt, exit tilts back -maxTilt.
maxBlurnumber8Maximum blur (px) at the entry and exit poses.
roundedstring"0.375rem"CSS border-radius for the tile clipping mask. Accepts any CSS length value.
containerRefObject<HTMLElement | null>-Optional ref to a scrollable ancestor. When set, scroll progress and the loop sentinel are measured against it instead of the viewport.
maxCyclesnumberInfinityHard cap on total cycles when loop is on. Set a finite value to bound DOM growth in long sessions.
sectionPaddingstring"20vh"Vertical breathing room (margin + padding) around the grid. Use "0" or a small rem value when embedding in a bounded container.
classNamestring-Additional className applied to the outer <section>.

Features

  • Scroll-driven choreography: Each tile uses useScroll with a start end → end start offset so its tilt, blur, and translate are tied directly to its position in the viewport.
  • Symmetric entry / exit pose: Tiles tilt forward +maxTilt on entry, settle flat at the midpoint, then tilt back -maxTilt as they exit over the top edge.
  • Alternating sides: Even-indexed tiles drift in from the left, odd-indexed from the right, producing a paired editorial cadence.
  • Infinite loop (opt-in): With loop, an IntersectionObserver sentinel appends two more cycles whenever the user gets within ~1500px of the bottom.
  • Respects prefers-reduced-motion: Tiles render as static stills when the user has reduced motion enabled — no transforms, no filters, no blur.
  • Composed transforms: x, y, z, rotate, rotateX, and skewX are applied together via Framer Motion's hardware-accelerated transform pipeline; an inner scaleY adds a subtle vertical squash so the tilt reads as physical rather than flat.