Image Trail
PointerSmall images pop up along the pointer's path and fade behind it.
Makes an image turn liquid under the cursor and settle back. A velocity field on the GPU (WebGL2 stable fluids, no dye) displaces where the picture is sampled, with a touch of chromatic split along the flow. The picture is a real <img>, which keeps alt, lazy loading, srcset and the LCP candidate, and is what shows without JavaScript or WebGL. The canvas above it takes over only while the fluid moves and hands back once it has settled. object-fit: cover and the image's object-position are matched in the shader. A cross-origin image without CORS, or one larger than the GPU takes as a texture (8192 px a side on many devices), stays a plain image. Every constant is a custom property re-read when the element's style changes.

move your cursor across
Live, from the library itself: scroll, hover, navigate away and back. The demo is never gated.
Adjust · props are custom properties, set inline, so you edit the running instance
JavaScript with its libs
13.1 kB raw
2.3 kB raw of its own, 1.3 kB gzip. The rest is the runtime, the canvas helper, the GL core and the fluid solver, loaded once for every effect on the page.
CSS 4.7 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 56 · Firefox 51 · Safari 15; needs webgl2. Elsewhere: without WebGL2 or a colour-renderable float texture (EXT_color_buffer_float), for a cross-origin image served without CORS, or for an image past the GPU's texture size limit, the plain <img>.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/liquid-image<LiquidImage src="/poster.webp" alt="A poster: the word liquid over an orange disc in concentric rings" width={1440} height={720} class="aspect-[2/1] w-full" />Without a key the registry answers 401 with an upgrade message. Related free primitives: image-trail, tilt.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | ImageMetadata | none | The image: a URL, or an astro:assets import (its width and height are used). Its first load starts the GPU work; a later srcset candidate replaces the texture. |
alt | string | none | Alternative text, on the <img>. |
width / height / srcset / sizes / fetchpriority | img attributes | none | Passed to the <img>. |
loading | 'lazy' | 'eager' | 'lazy' | Loading of the <img>. Use eager (and fetchpriority="high") for an image above the fold. |
strength | number | 1 | How far the picture moves with the fluid: velocity × a 40 ms look-back × strength. |
chroma | number | 0.3 | Red and blue sampled apart along the flow, as a share of the displacement. 0 is none. |
radius | number | 0.2 | Splat radius as a share of the box. |
curl | number | 6 | Vorticity confinement: how much the liquid swirls. 0 is laminar. |
settle | number | 1.5 | How fast the picture settles, per second. 3 is stiff, 0.8 lingers. |
crossorigin | 'anonymous' | 'use-credentials' | none | Passed to the <img>. A cross-origin image needs CORS to be uploaded to the GPU; without it the plain image stays. |
The <img> stays: the loop never runs and the canvas is hidden (by the media query, and by the page's own motion switch through the runtime).
ClientRouterBound through the shared runtime: the loop, observers and pointer listener are released before the swap and the WebGL context is lost on purpose after it; five round trips leave one context. Under transition:persist the canvas keeps its context and starts again on the next page.
Replaces: LiquidImage / ImageTrail distortion (React Bits Pro) · WebGL image hover distortion (Codrops). See the migration table.
Small images pop up along the pointer's path and fade behind it.
A card leans toward the cursor in 3D, lifts slightly, catches a glare, and settles back on the active preset's spring when the cursor leaves.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown