Carousel 3D
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.
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.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
radius | number | 2.5 | How 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. |
cardWidth | number | 1.2 | Card width in stage units. Also sets the spacing along the rails, so wider cards spread the rails out too. |
cardHeight | number | 1.5 | Card height in stage units. Together with cardWidth this fixes the aspect ratio the corner rounding is measured against. |
backgroundColor | string | "#000000" | Fills the scroll track behind the stage. The stage itself is transparent, so this is the colour you actually see. |
className | string | "" | Appended to the outer scroll track. The track is a fixed four screens tall, so avoid setting a height here. |
scroller | string | Element | the page | The 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.
| Prop | Type | Default | Description |
|---|---|---|---|
images* | string[] | — | Image URLs, one per card. |
radius | number | 2.5 | Ring radius in stage units. |
cardWidth | number | 1.2 | Card width in stage units. |
cardHeight | number | 1.5 | Card height in stage units. |
backgroundColor | string | "#000000" | Fills the scroll track behind the stage. The stage itself is transparent, so this is the colour you actually see. |
scrollProgressRef | React.RefObject<number> | internal ref | Holds 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. |
rotationRef | React.RefObject<number> | internal ref | Holds 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. |
scroller | string | Element | the page | The element that actually scrolls, when it isn't the page. |
className | string | "" | 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. |
ssr: false.