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.
Bring this into your project
Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.
npx webberui@latest add liquid-metal.enLiquid Metal is a React liquid metal (chrome) background component that draws a flowing metallic surface pixel by pixel on a canvas 2D context. The wrapper is styled with Tailwind CSS, no WebGL or shaders are involved, and Motion is used only to detect the reduced-motion setting.
Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<LiquidMetal/>
The complete example includes the preview layout and data. Install the component first, then paste this file into your project.
When to use
- The hero background of a product launch page, behind a large headline.
- Sections introducing hardware, audio gear, or a premium plan that call for a metallic backdrop.
- Event or countdown banners, with
colorsswitched to gold or copper tones. - Small decorative areas the size of a card or button, with a lower
resolutionto save more work.
Installation
npx webberui@latest add https://webberui.com/r/liquid-metal.en.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>Examples
Hero background
Use LiquidMetal as the section container, put the headline and button in children, and set the layout in className.
import { LiquidMetal } from "@/components/ui/liquid-metal";
export function LaunchHero() {
return (
<LiquidMetal className="flex min-h-[480px] flex-col items-center justify-center gap-6 px-6 text-center">
<h1 className="text-5xl font-semibold text-white mix-blend-difference">
The next audio interface
</h1>
<a href="/preorder" className="rounded-full bg-white px-5 py-2 text-sm font-medium text-black">
Join the preorder list
</a>
</LiquidMetal>
);
}Copper palette with finer texture
colors is shadow, midtone, and highlight in that order; a higher scale makes the texture denser and a lower reflections gives fewer bright bands.
<LiquidMetal
colors={["#1c0f0a", "#9a3412", "#fed7aa"]}
specular="#fff7ed"
scale={5}
reflections={2}
speed={0.6}
className="h-[280px] rounded-2xl"
/>Small card at a lower resolution
resolution is the width in px of the internal drawing buffer; a small card can use a lower value, and CSS scales the result up smoothly.
<LiquidMetal resolution={96} className="flex h-40 w-72 items-end rounded-xl p-4">
<span className="text-sm font-medium text-white">Pro plan</span>
</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
FAQ
Does it work with the Next.js App Router?
Yes. The source already has "use client", so import it from a Server Component page directly; the canvas is only drawn inside an effect, and the server renders an empty container.
Does it keep rendering after it scrolls out of view?
No. An IntersectionObserver cancels the requestAnimationFrame loop when the canvas leaves the viewport and restarts it when it comes back. To lower the cost further, reduce resolution.
Can colors use rgb() or Tailwind color names?
No. colors and specular only parse hex colors in #rgb or #rrggbb form; anything else falls back to a neutral gray.
Does the ripple work on phones?
No. The interactive ripple only responds when pointerType is mouse; touch input is ignored.
What happens when the user prefers reduced motion?
A single static metal surface is drawn and the animation loop never starts.