Particle Morph Gallery

ProScroll3DWebGLpowered by @vectorvesper/motion

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.

Particle Morph Gallery

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.

PropTypeDefaultDescription
images*ImageItem[]—The photos and their captions, in order. Required — nothing renders without it.
verticalLinesnumber20How many strands each photo breaks into. Fewer reads as torn ribbons; more as a fine dissolve.
scrollLengthstring"160vh"Height of the scroll track, as any CSS length. Longer gives more room per transition, so each one runs slower.
scrollerstring | HTMLDivElement | nullthe pageThe element that actually scrolls, when it isn't the page. Leave unset for the normal case.
backgroundColorstring"#CFCDCB"Colour behind the gallery. A warm off-white by default, not black.
textColorstring"#1a1a1c"Colour of the captions.
accentColorstring"#CD0921"Highlight colour used by the index and rules.
dprnumber | [number, number]device defaultPixel-ratio ceiling. A single number is an upper bound; a pair sets a range. Lower it if the section feels heavy.
classNamestring""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.
disableAdaptiveQualitybooleanfalseKeeps 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 />

type

One 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.

PropTypeDefaultDescription
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.
The morph is scroll-driven and has no reduced-motion state. It only moves when the visitor scrolls, but the break-up itself is large-area motion — consider a plain gallery when the preference is set.
Raising verticalLines costs more than it looks: each strand samples the photo independently. If the section feels heavy, lower dpr before lowering the strand count — it preserves the look better.
Client component. It opens a 3D drawing context on mount — import it dynamically with ssr: false if your route renders on the server.
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. Worth knowing about mount="auto" here: deferring the scene build leaves the thread to the image loader, so the pictures often appear sooner than they do with the default. 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