Three.js Image Gallery: 3D Photo Walls and Scroll Galleries

A Three.js image gallery is typically a grid or wall of textured plane meshes (one PlaneGeometry per image with the photo as its texture map), arranged in 3D space and navigated either by moving the camera through them (a scroll-driven fly-through) or by animating the planes themselves. This is the technique behind most "3D portfolio gallery" and WebGL photo-wall effects.

Last updated . Verified against three.js r181.

Read the FAQJump to code

The plane-per-image approach

Each image is a PlaneGeometry sized to the image's aspect ratio, with the photo loaded as a map texture on a MeshBasicMaterial (unlit, since gallery images usually shouldn't respond to scene lighting like a 3D object would) or a custom shader for hover/transition effects. Arrange them in a grid (a simple nested loop setting x/y positions with consistent spacing) or a more organic scattered layout, depending on the desired look.

For hover effects, the common "image scales up and brightens on hover" pattern, raycasting detects which plane the pointer is over, and the response is either a simple scale/opacity tween (GSAP, see the animation guide) or a shader-driven distortion for a more elaborate effect.

Scroll-driven navigation

The common "scroll to move through a 3D gallery" pattern maps scroll position (or a virtual scroll value from a smooth-scroll library like Lenis, which this site itself uses) to camera position or rotation, rather than relying on the browser's native scroll to move actual DOM content. The gallery images stay fixed in 3D space; scrolling moves the camera through them, which is what creates the "flying through a 3D space" feel distinct from a normal 2D scrolling image grid.

Texture loading strategy for many images

Loading dozens of full-resolution images up front stalls the initial page load. The standard mitigation: load a small, blurred or low-resolution placeholder texture immediately for every plane, then lazy-load the full-resolution texture for each image only as it approaches the camera's view (or the user scrolls near it), swapping the material's map once the full texture finishes loading. This keeps initial load fast regardless of total gallery size, and is the same progressive-loading principle behind lazy-loaded <img> tags, applied to 3D textures instead.

See it in production

Browse the full showcase →

Code

A simple plane grid gallery (r181)
1const loader = new THREE.TextureLoader()
2const images = ['/gallery/1.jpg', '/gallery/2.jpg', '/gallery/3.jpg']
3const cols = 3
4const spacing = 2.2
5
6images.forEach((url, i) => {
7 const texture = loader.load(url)
8 const plane = new THREE.Mesh(
9 new THREE.PlaneGeometry(2, 1.3),
10 new THREE.MeshBasicMaterial({ map: texture })
11 )
12 plane.position.set((i % cols) * spacing, -Math.floor(i / cols) * spacing, 0)
13 scene.add(plane)
14})
Hover distortion with TSL
1import { texture, uv, uniform, vec2 } from 'three/tsl'
2
3const hoverStrength = uniform(0) // tweened toward 1 on pointer enter/leave
4const distortedUv = uv().add(vec2(0, hoverStrength.mul(0.02)).mul(uv().y.sub(0.5)))
5
6const material = new THREE.MeshBasicNodeMaterial()
7material.colorNode = texture(imageTexture, distortedUv)

Learn this properly

Three.js + GSAP

Why GSAP Changes Everything

GSAP drives the hover and scroll-tween effects common in a 3D image gallery.

Start the lesson (8 minutes)

Frequently asked questions

How do I build a 3D image gallery in Three.js?

Create a PlaneGeometry per image with the photo as its texture, arrange them in a grid or scattered layout in 3D space, and use raycasting to detect hover/click for interaction effects.

How do I make a Three.js gallery scroll-driven?

Map scroll position (native or from a smooth-scroll library) to camera position or rotation rather than moving the images themselves. The gallery stays fixed in 3D space and the camera moves through it as the user scrolls.

How do I avoid slow load times with many gallery images?

Load small placeholder textures for every image up front, then lazy-load the full-resolution texture for each image only as it comes near the camera or into view, swapping the material's map once it finishes loading.

Keep reading