Liquid Metal Button
A polished-chrome rim with chromatic dispersion that ripples like liquid metal.
The rim stacks two conic gradients masked to a ring: a metallic one that reads as
turning chrome, and a faint spectrum one blended on top so the bright edges split
into a prism. An SVG feTurbulence to feDisplacementMap filter ripples the ring
like liquid, and a specular streak sweeps across on hover. Built with
Motion.
Installation
Complete the shared Setup first, then copy the component into
components/ui/liquid-metal-button.tsx.
bun add motion'use client';import * as React from 'react';import { useReducedMotion } from 'motion/react';import { cn } from '@/lib/cn';const FOCUS = 'focus-visible:outline-none focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-color:var(--lm-focus)]';const RING_MASK = '[-webkit-mask:linear-gradient(#fff_0_0)_content-box,linear-gradient(#fff_0_0)] [-webkit-mask-composite:xor] [mask-composite:exclude]';export type LiquidMetalButtonProps = { rimWidth?: number; speed?: number; prism?: number; liquid?: boolean; liquidScale?: number; tone?: 'chrome' | 'gold'; accent?: string; icon?: React.ReactNode;} & React.ButtonHTMLAttributes<HTMLButtonElement>;export function LiquidMetalButton({ rimWidth = 3, speed = 6, prism = 0.5, liquid = true, liquidScale = 6, tone = 'chrome', accent = '#cdd4dc', icon, children = 'Build your website now', className, style, ...props}: LiquidMetalButtonProps) { const reduced = useReducedMotion(); const id = React.useId().replace(/[^a-zA-Z0-9]/g, ''); const filterId = `lm-liquid-${id}`; const vars = { '--lm-speed': `${speed}s`, '--lm-prism': prism, '--lm-focus': accent, } as React.CSSProperties; const rimFilter = liquid ? `url(#${filterId})` : undefined; return ( <button type="button" data-tone={tone} style={{ ...vars, ...style }} className={cn( 'lm-btn group relative isolate inline-flex cursor-pointer select-none rounded-full', 'shadow-[0_1px_2px_0_rgba(0,0,0,0.18),0_10px_28px_-12px_rgba(0,0,0,0.35)]', 'dark:shadow-[0_1px_2px_0_rgba(0,0,0,0.55),0_14px_36px_-12px_rgba(0,0,0,0.7)]', 'transition-transform duration-100 ease-out active:scale-[0.98] motion-reduce:active:scale-100', FOCUS, className, )} {...props} > {liquid && ( <svg aria-hidden width="0" height="0" className="absolute" style={{ position: 'absolute' }} > <defs> <filter id={filterId} x="-30%" y="-30%" width="160%" height="160%" colorInterpolationFilters="sRGB" > <feTurbulence type="fractalNoise" baseFrequency="0.012 0.018" numOctaves={2} seed={4} result="noise" > {!reduced && ( <animate attributeName="baseFrequency" dur="18s" values="0.012 0.018;0.018 0.012;0.012 0.018" repeatCount="indefinite" /> )} </feTurbulence> <feDisplacementMap in="SourceGraphic" in2="noise" scale={liquidScale} xChannelSelector="R" yChannelSelector="G" /> </filter> </defs> </svg> )} <span aria-hidden className={cn( 'pointer-events-none absolute inset-0 overflow-hidden rounded-[inherit]', RING_MASK, )} style={{ padding: rimWidth }} > <span className="lm-metal absolute inset-[-30%]" style={{ filter: rimFilter }} /> <span className="lm-spectrum absolute inset-[-30%]" style={{ filter: rimFilter }} /> </span> <span aria-hidden className="lm-sheen pointer-events-none absolute inset-0 rounded-[inherit]" /> <span className={cn( 'relative z-10 flex items-center justify-center gap-2.5 rounded-full px-6 py-3', 'text-[15px] font-medium tracking-[-0.01em]', 'bg-gradient-to-b from-white to-[#eceef1] text-neutral-900', 'dark:from-[#1c1c1e] dark:to-[#0e0e0f] dark:text-white', 'shadow-[inset_0_1px_0_0_rgba(255,255,255,0.7)]', 'dark:shadow-[inset_0_1px_0_0_rgba(255,255,255,0.07)]', )} style={{ margin: rimWidth }} > {icon != null && ( <span className="flex shrink-0 items-center justify-center [&_svg]:size-[18px]"> {icon} </span> )} {children} </span> </button> );}Add the animations to your global.css.
@property --lm-angle {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
@keyframes lm-spin {
to {
--lm-angle: 360deg;
}
}
.lm-metal {
background: conic-gradient(
from var(--lm-angle),
#f6f8fb 0%,
#aeb6c0 8%,
#565d67 20%,
#cdd4dc 30%,
#ffffff 40%,
#99a1ab 52%,
#434a53 64%,
#c6cdd6 74%,
#f6f8fb 84%,
#8b939d 92%,
#f6f8fb 100%
);
animation: lm-spin var(--lm-speed, 6s) linear infinite;
}
.lm-btn[data-tone='gold'] .lm-metal {
background: conic-gradient(
from var(--lm-angle),
#fff4d6 0%,
#d9ad5b 10%,
#7c5f22 22%,
#f3d488 32%,
#fffbe9 42%,
#c79a44 54%,
#6e521c 66%,
#e9c878 76%,
#fff4d6 86%,
#b58f3c 94%,
#fff4d6 100%
);
}
.lm-spectrum {
background: conic-gradient(
from calc(var(--lm-angle) * -1.5),
#ff2d55,
#ff9500,
#ffd60a,
#34c759,
#00c7be,
#0a84ff,
#bf5af2,
#ff2d55
);
mix-blend-mode: screen;
opacity: var(--lm-prism, 0.5);
animation: lm-spin var(--lm-speed, 6s) linear infinite;
}
@keyframes lm-sheen {
to {
background-position: -150% 0;
}
}
.lm-sheen {
background: linear-gradient(
100deg,
transparent 38%,
rgba(255, 255, 255, 0.55) 50%,
transparent 62%
);
background-size: 250% 100%;
background-position: 150% 0;
opacity: 0;
mix-blend-mode: overlay;
}
.dark .lm-sheen {
mix-blend-mode: screen;
}
@media (hover: hover) and (pointer: fine) {
.lm-btn:hover .lm-sheen {
opacity: 1;
animation: lm-sheen 720ms cubic-bezier(0.23, 1, 0.32, 1);
}
}
@media (prefers-reduced-motion: reduce) {
.lm-metal,
.lm-spectrum {
animation: none;
}
.lm-btn:hover .lm-sheen {
animation: none;
}
}Usage
import { Sparkles } from 'lucide-react';
import { LiquidMetalButton } from '@/components/ui/liquid-metal-button';
export default function Example() {
return (
<LiquidMetalButton icon={<Sparkles strokeWidth={1.75} />}>
Build your website now
</LiquidMetalButton>
);
}Examples
A warm gold tone for a different call to action.
Turn off the liquid ripple for a clean, polished rim.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
rimWidth | number | 3 | Rim thickness in pixels. |
speed | number | 6 | Seconds for one full turn of the chrome. |
prism | number | 0.5 | Strength of the chromatic dispersion overlay, 0-1. |
liquid | boolean | true | Molten ripple of the metal edge. |
liquidScale | number | 6 | Displacement amount of the liquid ripple, in pixels. |
tone | "chrome" | "gold" | "chrome" | Rim palette. |
accent | string | "#cdd4dc" | Focus-outline color. |
icon | React.ReactNode | - | Optional leading icon. |
className | string | - | Forwarded to the button. |
The button also accepts every native <button> prop.