Liquid Metal
A reflective liquid chrome / flowing metal surface drawn live on a 2D canvas, with specular striping and optional mouse ripples.
Flowing liquid metal simulated by a domain-warped multi-layer sine field, mapped onto a three-stop color ramp with sharpened specular bands layered over it, drawn on a low-resolution buffer and then smoothly upscaled by CSS.
Loading preview…
npx shadcn@latest add https://webberui.com/r/liquid-metal.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
1
3
0.6
3
<LiquidMetal />
Installation
npx shadcn@latest add https://webberui.com/r/liquid-metal.jsonOr, once registries are configured in components.json, install it as @webberui/liquid-metal.
Usage
Put your foreground content in children, and write the layout (centering, height) on className:
import { LiquidMetal } from "@/components/ui/liquid-metal";
<LiquidMetal className="flex h-[400px] items-center justify-center rounded-2xl">
<h1 className="text-4xl font-bold text-white mix-blend-difference">
Chrome
</h1>
</LiquidMetal>Custom color ramp, mouse ripples enabled, and a faster flow:
<LiquidMetal
colors={["#1a1005", "#b45309", "#fef3c7"]}
specular="#fffbeb"
interactive
speed={1.4}
distortion={0.8}
className="flex h-[320px] items-center justify-center rounded-2xl"
>
<p className="text-lg text-white mix-blend-difference">Molten gold</p>
</LiquidMetal>Props
| Prop | Type | Default | Description |
|---|---|---|---|
colors | [string, string, string] | Chrome silver palette | Three metal color stops: [shadow, midtone, highlight] |
specular | string | "#f8fafc" | Color of the specular reflection bands |
speed | number | 1 | Flow speed multiplier |
scale | number | 3 | Spatial frequency — higher makes the surface texture denser |
distortion | number | 0.6 | Domain warp strength — higher makes it feel more liquid |
reflections | number | 3 | Density of the specular bands |
interactive | boolean | false | The mouse drags ripples across the surface (touch has no effect) |
resolution | number | 192 | Width of the internal drawing buffer — smaller is cheaper |
children | React.ReactNode | — | Foreground content, layered above the metal surface |
className | string | — | Appended to the outermost container's className |
How it works
- Low-resolution buffer + CSS upscaling. The per-pixel math runs on a discrete buffer of fixed width (
192by default); afterputImageData, CSS stretches the canvas to fill the container, and the browser's bilinear interpolation naturally smears the pixels into a smooth liquid surface — the cost is independent of the container's actual size - The surface is a continuous field built from three stacked sine layers, with the coordinates themselves displaced by another sine (domain warping) to produce the offset flow; the specular striping is a high-frequency sine of the field value sharpened with
pow4, layered over the three-stop ramp to give chrome its reflection - The buffer's aspect ratio follows the container through a
ResizeObserver, so the metal texture never gets stretched out of shape; on teardown,cancelAnimationFrameanddisconnectare cleaned up together - Props that can change are stored in refs and the animation loop reads the latest value each frame, so changing a parameter never restarts
requestAnimationFrame - With
interactiveon, the cursor position is normalized to 0~1 and injects a ring-shaped ripple at that point with a Gaussian falloff over distance; it only applies whenpointerType === "mouse", and the ripple fades out once the cursor leaves - When the user has "reduce motion" enabled at the system level, only a single static metal surface is drawn and the animation loop never starts