Carousel 3D

ProScroll3DWebGL

A cylindrical 3D carousel built on React Three Fiber — images arranged around a configurable radius with click-to-focus. Exposes rotation and scroll-progress refs so external UI can read the carousel's state without re-rendering.

Carousel 3D

Included with membership

Members install Carousel 3D with one command and get the full source, the usage code and the Framer remix, licensed for client work. One membership covers the whole library and the CLI.

When to use it

A portfolio or product showcase where you want the whole set visible at once as an object, rather than one item at a time. The scroll track is a fixed four screens tall and the shape changes across it, so give it a section of its own — it does not sit politely inside a page of other content.

  • Use an even number of images — the rails take alternating cards, so an odd count leaves one rail short.
  • Size the source images near the size they are drawn at; each card is its own texture.
  • Raise radius together with cardWidth, or the cards start intersecting as the ring tightens.
  • Pass scroller when the carousel lives in a nested scrolling container rather than on the page.

Avoid:

  • Do not set a height on it through className. The track is a fixed four screens and overriding it desynchronises the shape from the scroll.
  • Do not host the images somewhere that omits CORS headers — they are uploaded as textures, and a cross-origin file without permissive headers renders as blank cards with no error.
  • Do not put it in a page that already pins a section at the same scroll range.

Props

A ring of images floating in 3D that unwinds as you scroll. It starts as a rotating cylinder, then unrolls into two diagonal rails running in opposite directions, which keep moving on their own once you reach the bottom. Click any image to open it full-screen.

<Carousel3D />

The whole effect in one tag — scroll track, 3D stage and click-to-expand included.

PropTypeDefaultDescription
images*string[]—Image URLs, one per card. Any count works; the ring divides a full turn evenly between them and the rails take alternating cards, so even numbers split cleanly.
radiusnumber2.5How far the cards sit from the centre of the ring, in stage units. Larger values push the ring wider and further from the camera, which sits at a distance of 4.5.
cardWidthnumber1.2Card width in stage units. Also sets the spacing along the rails, so wider cards spread the rails out too.
cardHeightnumber1.5Card height in stage units. Together with cardWidth this fixes the aspect ratio the corner rounding is measured against.
backgroundColorstring"#000000"Fills the scroll track behind the stage. The stage itself is transparent, so this is the colour you actually see.
classNamestring""Appended to the outer scroll track. The track is a fixed four screens tall, so avoid setting a height here.
scrollerstring | Elementthe pageThe element that actually scrolls, when it isn't the page. Without it the carousel listens to the page scrollbar and stays a motionless ring inside a nested container.
mount"auto" | "eager""eager"When to build the scene. The default builds it as soon as the component mounts, which is what it has always done. Set "auto" to hand the decision to the runtime, which waits until the carousel is near the viewport and the page has frame headroom to spare. Worth setting when it sits below the fold. Neither setting takes it off the shared frame loop or overrides the reduced-motion preference. A scene running its own loop would be invisible to the scheduler, so it could not be set aside under load, and it would slow every other animation on the page rather than only itself.

<MorphingCanvas />

The stage on its own, with the scroll state exposed. Use it when you want the same visual but your own scroll wiring, background or click behaviour.

PropTypeDefaultDescription
images*string[]—Image URLs, one per card.
radiusnumber2.5Ring radius in stage units.
cardWidthnumber1.2Card width in stage units.
cardHeightnumber1.5Card height in stage units.
backgroundColorstring"#000000"Fills the scroll track behind the stage. The stage itself is transparent, so this is the colour you actually see.
scrollProgressRefReact.RefObject<number>internal refHolds scroll progress from 0 to 1 and decides the shape: below 0.3 it is a ring, 0.3 to 0.75 it unrolls, above 0.75 the rails drift. Pass your own ref to read the value, or to drive the shape from something other than scroll.
rotationRefReact.RefObject<number>internal refHolds the ring's angle in radians. Driven to two full turns across the scroll by default. Pass your own to read it, or to spin the ring yourself.
onCardClick(src: string) => void—Replaces the built-in full-screen viewer. Receives the clicked image's URL — supply this and the overlay never renders, so you can open your own lightbox or route instead.
scrollerstring | Elementthe pageThe element that actually scrolls, when it isn't the page.
classNamestring""Appended to the outer scroll track.
mount"auto" | "eager""eager"Same as on Carousel3D, which passes its own value straight through. Set it here when you are using the canvas on its own.
Client component. It opens a 3D drawing context on mount, which does not exist during a server render — import it dynamically with ssr: false.
This one does not soften for reduced motion. The whole component is continuous 3D movement, so there is no meaningful reduced state — render a plain grid instead when a visitor asks for less motion.
The scroll track is a fixed four screens tall and is not configurable. Every card is a separate textured quad, so keep the source images near the size they are drawn at rather than shipping full-resolution photographs.
Images are loaded cross-origin as textures. Host them somewhere that returns permissive CORS headers, or the cards render blank.
It draws on the shared frame loop rather than starting one of its own, so putting several animated components on a page costs one callback, not one each, and the scheduler can set decorative work aside when frames get tight. Resolution and shadow detail follow the device tier, decided once for the whole page rather than guessed per component. If the browser takes the drawing context away — which it does under memory pressure, and more often on a page with several of these — the component reports it and rebuilds once the context comes back, instead of leaving a permanently black rectangle.
Back to all components