Particle Morph Gallery
Images dissolved into particle fields that morph between shots on scroll, rendered through React Three Fiber. Strand count and a device-pixel-ratio cap are exposed so the effect can be dialled back on weaker hardware.
Included with membership
Members install Particle Morph Gallery 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 hero or a chapter break where the transition between images is the point, not the images themselves. It wants a handful of strong pictures and a lot of scroll; it is the wrong choice for a browsable gallery, because nothing can be reached without scrolling past everything before it.
- Space col values well apart — 1, 7, 13, 18 rather than 0, 1, 2, 3 — or every panel lands in the same narrow arc and they overlap.
- Give two panels sharing a column opposite y offsets, so one sits above the other.
- Raise scrollLength past the 160vh default when you have more than about six panels; the default runs them fast.
- Ship images near the size they are drawn at — every panel is sampled independently.
Avoid:
- Do not pass consecutive col values. This is the single most common way to get a broken-looking gallery, and it looks like a bug in the component rather than in the data.
- Do not use it as a navigable gallery. There is no way to jump to a panel.
- Do not raise verticalLines to sharpen the dissolve before lowering dpr — the strand count costs far more.
Props
A gallery where each photo dissolves into vertical strands and re-forms as the next one, driven by scroll. It reads as a printed page coming apart rather than a crossfade.
<ParticleMorphGallery />
The whole section, including its scroll track.
| Prop | Type | Default | Description |
|---|---|---|---|
images* | ImageItem[] | — | The photos and their captions, in order. Required — nothing renders without it. |
verticalLines | number | 20 | How many strands each photo breaks into. Fewer reads as torn ribbons; more as a fine dissolve. |
scrollLength | string | "160vh" | Height of the scroll track, as any CSS length. Longer gives more room per transition, so each one runs slower. |
scroller | string | HTMLDivElement | null | the page | The element that actually scrolls, when it isn't the page. Leave unset for the normal case. |
backgroundColor | string | "#CFCDCB" | Colour behind the gallery. A warm off-white by default, not black. |
textColor | string | "#1a1a1c" | Colour of the captions. |
accentColor | string | "#CD0921" | Highlight colour used by the index and rules. |
dpr | number | [number, number] | device default | Pixel-ratio ceiling. A single number is an upper bound; a pair sets a range. Lower it if the section feels heavy. |
className | string | "" | Appended to the outer section. |
mount | "auto" | "eager" | "eager" | When to build the scene. The default builds it on mount. Set "auto" to wait until the gallery is near the viewport and the page has frame headroom. Worth trying even above the fold: not building the scene while the images are still downloading leaves the whole thread to the loader, so the pictures often appear sooner than they do with the default. 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. |
disableAdaptiveQuality | boolean | false | Keeps the pixel ratio at whatever the device reports, up to 2. By default the runtime caps it on lower tiers. This is not the same as dpr, which pins a fixed value: this one still follows the device, it just stops the runtime throttling it. |
<ImageItem />
typeOne panel. The positional fields are in the gallery's own units, not pixels — they place the panel on the cylinder, and getting them wrong is the usual reason panels overlap.
| Prop | Type | Default | Description |
|---|---|---|---|
url* | string | — | The picture. |
col* | number | — | Which column of the cylinder the panel sits in. This is an angle, not an index — consecutive numbers bunch every panel into a narrow arc on one side. Space them out across the full range. |
y* | number | — | Vertical offset. Two panels sharing a column need different values here or they sit on top of each other. |
w* | number | — | Panel width. |
h* | number | — | Panel height. |
label* | string | — | Caption shown under the panel. |
ssr: false if your route renders on the server.