liten

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.

Terminal
bun add motion
components/ui/liquid-metal-button.tsx
'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.

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

Example.tsx
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

PropTypeDefaultDescription
rimWidthnumber3Rim thickness in pixels.
speednumber6Seconds for one full turn of the chrome.
prismnumber0.5Strength of the chromatic dispersion overlay, 0-1.
liquidbooleantrueMolten ripple of the metal edge.
liquidScalenumber6Displacement amount of the liquid ripple, in pixels.
tone"chrome" | "gold""chrome"Rim palette.
accentstring"#cdd4dc"Focus-outline color.
iconReact.ReactNode-Optional leading icon.
classNamestring-Forwarded to the button.

The button also accepts every native <button> prop.

On this page0%