Refractive Gallery

ProShowcase & MediaWebGLpowered by @vectorvesper/motion

An infinite draggable gallery viewed through a refractive lens, with chromatic aberration, refraction strength, zoom and damping all exposed. Ships a small keyframe stylesheet alongside the component.

Refractive Gallery

Included with membership

Members install Refractive 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 landing surface or an archive that is meant to be wandered rather than read — the grid has no beginning or end and the lens rewards moving the pointer around. It is a mood piece, not navigation: nothing about it tells a visitor where they are or how much there is.

  • Ship a small copy of every image at <dir>/thumbs/<name>.webp. The grid tiles come from there, not from the paths you pass.
  • Give the parent a real height — the gallery fills its container and an unsized parent collapses it to nothing.
  • Pass a handful of images. They repeat to fill the grid, so more variety costs texture memory for little gain.
  • Raise radius for a heavier, slower lens and lower it for a tight loupe.

Avoid:

  • Do not pass paths without the thumbs copies in place. Every tile 404s, the grid renders black, and the lens still works — which makes it look like a shader bug rather than a missing file.
  • Do not use it where someone needs to find a specific image. The grid wraps forever and there is no ordering to rely on.
  • Do not put it inside a scrolling column; it captures the wheel to pan, and the page underneath stops scrolling.

Props

An endless grid of thumbnails with a glass lens following the cursor. Whatever passes under the lens is magnified and split into colour at its edges. The grid wraps in every direction, so there is no end to scroll to.

<RefractiveInfiniteGallery />

The grid and its lens. Fills its container, so give the parent a height.

PropTypeDefaultDescription
images*string[]—Pictures to fill the grid. Required. They repeat to cover the full grid, so a handful is enough. Each one is drawn twice, at two sizes — see the note on the small copies below, because the grid renders black without them.
zoomnumber1.5How much the lens magnifies what is under it.
radiusnumber260Size of the lens in pixels.
dampingnumber0.30How quickly the lens follows the cursor. Lower makes it trail further behind; 1 pins it to the pointer.
chromaticAberrationnumber0.014How far the colour channels separate at the lens edge. Small values read as real glass; large ones as an effect.
refractionnumber0.0How much the lens bends what is under it, on top of magnifying. Off by default — raise it for a rounder, heavier lens.
itemSizenumber50Size of one thumbnail in the grid.
gapnumber20Space between thumbnails.
edgeFadebooleantrueFades the grid out toward the edges of the container so it does not stop abruptly.
classNamestring""Applied to the wrapper.
mount"auto" | "eager""eager"When to build the lens. The default builds it on mount. Set "auto" to wait until the gallery is near the viewport and the page has frame headroom. 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.
Every picture is used at two sizes: the full file under the lens, and a small copy for the grid tiles. The small copy is not something you pass — it is derived from the path you gave, by inserting a thumbs folder and switching the extension to .webp. /gallery/car.png is read as /gallery/thumbs/car.webp. If that file is not there the tile request 404s and the tile stays fully transparent, which shows up as a black grid with a working lens — the single most common way to get this component wrong. Either ship the small copies alongside the originals, or pass paths that already sit in a thumbs folder, which are used as-is for both.
The lens only moves with the cursor — nothing animates on its own, so there is no ambient motion to suppress. It has no explicit reduced-motion state.
The grid is drawn as one repeating surface rather than as individual elements, so cols and rows are inexpensive. The lens is the cost, and it scales with radius.
Client component. It opens a GPU drawing context and reads the pointer, so it has to run in the browser.
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. 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