Building a liquid-metal UI kit for React
After shipping Glacé — the
liquid-glass kit — I wanted the opposite material. Glass bends the world behind
it. Metal reflects a world that isn't there. So:
Argent (npm i argentui),
chrome, gold, gunmetal, and obsidian surfaces that flow like mercury. Everything
below is the real package running live. Hover the tiles.
CSS couldn't fake this one
Glacé works in pure CSS + SVG because glass is refraction — you displace the
actual backdrop with an feDisplacementMap and the eye believes it. I tried the
same trick for metal: a seven-stop chrome gradient, animated, warped through
feTurbulence. It looked like what it was — a gradient with a wobble.
The difference is that metal is reflection. A chrome surface shows you a warped environment — bright sky bands, dark horizon bands — and that environment has to come from somewhere. There's nothing behind the element to borrow. You have to synthesize the world, and that's a fragment shader's job.
The best implementation of this on the web is Paper's liquid-metal shader — it's what the chrome-logo demos you've seen lately are built on. The recipe inside is worth knowing: a hard light→dark stripe ramp (the faux environment), a curvature term that bends stripes as if wrapping a bulged surface, simplex noise scrolling through the coordinate space (the "liquid"), and the stripes sampled three times at small offsets — once per color channel — which gives the prismatic fringing that reads as polished chrome.
A shader is not a component
Paper gives you a <LiquidMetal> canvas. A kit needs buttons. The bridge is
one idea: run the shader with shape="none" so it fills the element edge to
edge, drop the canvas behind your content, and clip it to the surface's radius.
Tones are just tuned parameter sets. Suddenly the shader is a material you
can apply to anything:
import { MetalButton, MetalCard } from "argentui";
import "argentui/styles.css";
<MetalButton tone="silver">Get started</MetalButton>
<MetalCard tone="gunmetal" revealOnHover>…</MetalCard>The first version filled every surface with chrome and it was a lot — like a
website made entirely of hubcaps. The fix became Argent's defining default:
the metal is the edge, not the background. A quiet dark panel with a
liquid-metal rim, and revealOnHover floods the surface on interaction. Restraint
at rest, spectacle on demand. (Full chrome is still there via variant="fill".)
The browser will eat your canvases
Here's the thing nobody tells you about building a kit where every component is a WebGL canvas: browsers cap concurrent WebGL contexts at around 16. When I added the fifth demo section to the docs site, the page hit 23 canvases and Chrome started silently killing contexts — seven surfaces just went black. No error, no event you'd notice. Your oldest contexts simply stop existing.
The fix is to treat contexts like the scarce resource they are. Each surface
mounts its shader only while it's on or near the screen (an
IntersectionObserver with a 250px margin) and releases the context the moment
it scrolls away, with a static gradient standing in. The same page now peaks at
six live canvases while scrolling, zero lost. If you ship anything
WebGL-per-component, you need this — and it has a happy side effect: offscreen
metal costs nothing.
One more bug for the road: my surfaces had overflow: hidden to clip the
canvas, and any metal element inside a flex row collapsed to 2px wide.
overflow other than visible zeroes a flex item's automatic minimum size.
The canvas was already clipped by its own wrapper, so the outer overflow
was doing nothing except destroying layout. Two days of "why is the nav
shrunk" for one deleted line.
Mercury as a control surface
The fun part of a metal kit is that liquid metal wants to be interactive.
The toggle's thumb is a drop of mercury that squishes (scale(1.12, 0.9))
while you hold it. The progress bar is molten fill rising in a dark channel.
Text gets both treatments. The default is a metallic gradient with
background-clip: text — pure CSS, costs nothing, works everywhere. But pass
shader and the component measures your string, renders it into an SVG
silhouette, and pours the actual shader into the letterforms — the same
liquid-logo treatment, for type. There's also variant="outline", the border
idea applied to text: the metal runs around the edge of each glyph while the
interior takes a dark or gradient fill (the second and third lines below — the
gradient stands in until the shader loads):
There's also MetalLogo — pass any image with a transparent background and
the metal pours into its silhouette, which is the classic use of this shader
(it processes your image into a height field and flows the bands inside it).
And the buttons vibrate on press where the platform allows it
(navigator.vibrate, so Android; iOS Safari pretends not to hear you).
The license, and writing my own shader
One honest wrinkle. Paper's shaders ship under PolyForm Shield — free to
use, but with a noncompete clause, and "a component library built on your
shader library" lives uncomfortably close to the line. Argent keeps
@paper-design/shaders-react as a peer dependency (you install it; I don't
redistribute it), which is the polite reading. But the real way out is to not
need it.
So Argent ships its own engine: about 120 lines of clean-room GLSL written
from the recipe, not the source — stripe ramp, curvature, two octaves of
simplex noise, per-channel dispersion, rim lift. Pass engine="native" to any
surface and there's no Paper code in your bundle at all, MIT end to end:
This rim is Argent's own ~120-line WebGL2 shader — no Paper dependency. Banding, noise flow, dispersion, rim. MIT all the way down.
The native engine is already indistinguishable on rims and borders; Paper's still wins on full-bleed fills (their dispersion and edge handling have had a lot more love). It stays opt-in until that gap closes, and then it becomes the default and the peer dependency becomes optional.
Caveats, because every shiny thing has them
- Every metal surface is a WebGL canvas. The viewport gating makes this fine in practice, but a grid of 200 metal buttons is still a bad idea. Use metal where it earns its keep.
prefers-reduced-motionfreezes the flow (the surfaces still look like metal, just still). Haptics are opt-out viasetHaptics(false).- SSR renders a static gradient until the canvas mounts — fine for below-fold content, a brief visible swap if a metal hero is your LCP element.
The kit: npm i argentui ·
argentui docs + lab ·
GitHub · glass sibling:
Glacé.