Skip to example

Scroll velocity: 3D planes

An example of creating a 3D gallery with a scroll velocity-linked wave and a detail view, using the Motion+ Gallery component for React.

This scroll velocity effect bends a 3D carousel of planes into a wave as you scroll. useVelocity reads the velocity of a spring-smoothed scroll value, and useSpring turns it into each plane's wave offset. useTransform builds each plane's 3D transform from its position and the wave.

React

Source code

Tutorial

Tutorial time
5 min
Difficulty

Introduction

The Scroll velocity: 3D planes example lays a collection of photos out as a line of angled planes that rises into the distance. Scroll or drag it, and a wave ripples through the planes with a strength that follows your speed. Hover a plane and it lifts towards you with a scrambled label. Click it and it opens into a detail view, with the rest of the collection lined up in a strip below.

The Motion+ Gallery component does the heavy lifting. Its list arrangement places the planes, the ripple modifier makes the wave, and galleryDetail opens the detail view. We also use AnimatePresence, useTransform and the Motion+ ScrambleText component for the labels.

Get started

Let's start with the page: a header, a hint and a list of plane components, each with an image and an index.

"use client"

const images = Array.from({ length: 16 }, (_, i) => `/photos/${i + 1}.jpg`)
const labels = ["Afterglow", "Drift Frame", "Peripheral" /* ... */]

function Plane({ index }) {
    return (
        <div className="plane">
            <img
                src={images[index]}
                alt={labels[index]}
                className="plane-image"
                draggable={false}
            />
            <div className="plane-index">{String(index).padStart(2, "0")}</div>
        </div>
    )
}

export default function ScrollVelocityLinkedOffset() {
    return (
        <>
            <div className="heritage">
                <div className="header">
                    <div className="title">HERITAGE FW25/26</div>
                    <div className="title title-collection">COLLECTION</div>
                </div>
                <div className="hint">scroll to surf, click to open</div>
                <div className="viewport">
                    {images.map((_, index) => (
                        <Plane key={index} index={index} />
                    ))}
                </div>
            </div>
            <Stylesheet />
        </>
    )
}

function Stylesheet() {
    return (
        <style>
            {/** Copy styles from example source code */}
        </style>
    )
}

Right now the planes sit in a plain row. In the next steps, Gallery turns them into a 3D line that we can scroll, drag and open.