Liquid Metal
Flowing chrome: a warped height field reflecting a procedural studio, in silver, gold, titanium, black chrome or oil-slick.
A capsule CTA with a live chrome rim that ripples from your touch — plus a zero-JS CSS chrome variant.
Browser support · WebGL2 / WebGL1. Falls back to MetalButtonCSS (gradients + sweep) without WebGL.
npx shadcn@latest add https://freedesignmd.com/r/liquid-metal-button.json1/**2 * LiquidMetalButton — a capsule CTA whose rim (or whole body) is live liquid3 * chrome. A WebGL canvas renders the metal behind a CSS-masked capsule so the4 * label stays crisp DOM text. Pressing sends a ripple out from the touch point.5 *6 * Also exports MetalButtonCSS — a zero-JS fallback with conic/linear chrome7 * gradients and an animated specular sweep. Same API, works everywhere.8 *9 * Props (both)10 * - variant?: "rim" | "solid" Metal only on the ring, or the whole button. Default "rim".11 * - tint?: "silver" | "gold" | "titanium" | "black-chrome". Default "silver".12 * - size?: "md" | "lg". Default "lg".13 * - icon?: ReactNode Trailing icon.14 * - plus every native <button> prop.15 * CSS vars: --fd-lmbtn-ring (rim thickness), --fd-lmbtn-face (inner fill), --fd-lmbtn-ink (label colour).16 *17 * Browser support: WebGL2 / WebGL1; without WebGL the CSS chrome is used.18 * Accessibility: native <button>, visible focus ring; canvas is aria-hidden.19 * Reduced motion: still metal, no ripple, no sweep.20 */21import { forwardRef, useEffect, useRef, useState } from "react";22import type { ButtonHTMLAttributes, CSSProperties, MutableRefObject, ReactNode, RefObject } from "react";23 24/* ───────────────────────── tiny WebGL stage ───────────────────────── */25 26type GL = WebGLRenderingContext | WebGL2RenderingContext;27interface Stage {28 gl: GL;29 gl2: boolean;30 prog: WebGLProgram;31 u: Record<string, WebGLUniformLocation | null>;32 w: number;33 h: number;34 dpr: number;35 reduced: boolean;36}37interface StageHooks {38 setup?: (s: Stage) => void;39 /** Set uniforms; return true to keep animating. */40 frame: (s: Stage, time: number, dt: number) => boolean;41}42 43function createStage(canvas: HTMLCanvasElement, frag: string): Stage | null {44 const attrs: WebGLContextAttributes = { antialias: false, alpha: true, premultipliedAlpha: true, powerPreference: "high-performance" };45 let gl: GL | null = canvas.getContext("webgl2", attrs) as WebGL2RenderingContext | null;46 const gl2 = !!gl;47 if (!gl) gl = (canvas.getContext("webgl", attrs) || canvas.getContext("experimental-webgl", attrs)) as WebGLRenderingContext | null;48 if (!gl) return null;49 const head = gl2 ? "#version 300 es\n" : "";50 const vs = head + (gl2 ? "#define attribute in\n" : "") + "attribute vec2 aP;void main(){gl_Position=vec4(aP,0.,1.);}";51 const fs =52 head +53 "#ifdef GL_FRAGMENT_PRECISION_HIGH\nprecision highp float;\n#else\nprecision mediump float;\n#endif\n" +54 (gl2 ? "#define texture2D texture\n#define gl_FragColor fdOut\nout vec4 fdOut;\n" : "") +55 frag;56 const make = (type: number, src: string) => {57 const s = gl!.createShader(type);58 if (!s) return null;59 gl!.shaderSource(s, src);60 gl!.compileShader(s);61 if (!gl!.getShaderParameter(s, gl!.COMPILE_STATUS)) {62 console.warn("[fd-shader]", gl!.getShaderInfoLog(s));63 return null;64 }65 return s;66 };67 const v = make(gl.VERTEX_SHADER, vs);68 const f = make(gl.FRAGMENT_SHADER, fs);69 const prog = gl.createProgram();70 if (!v || !f || !prog) return null;71 gl.attachShader(prog, v);72 gl.attachShader(prog, f);73 gl.bindAttribLocation(prog, 0, "aP");74 gl.linkProgram(prog);75 if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) return null;76 gl.useProgram(prog);77 const buf = gl.createBuffer();78 gl.bindBuffer(gl.ARRAY_BUFFER, buf);79 gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1, -1, 3, -1, -1, 3]), gl.STATIC_DRAW);80 gl.enableVertexAttribArray(0);81 gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);82 const u: Stage["u"] = {};83 for (const m of frag.matchAll(/uniform\s+\w+\s+(\w+)/g)) u[m[1]] = gl.getUniformLocation(prog, m[1]);84 return { gl, gl2, prog, u, w: 1, h: 1, dpr: 1, reduced: false };85}86 87function useStage(ref: RefObject<HTMLCanvasElement | null>, frag: string, hooks: MutableRefObject<StageHooks>, maxDpr = 2) {88 const [failed, setFailed] = useState(false);89 const kick = useRef<() => void>(() => {});90 useEffect(() => {91 const canvas = ref.current;92 if (!canvas) return;93 const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;94 let stage: Stage | null = null;95 let raf = 0;96 let last = 0;97 let time = 0;98 let visible = true;99 let alive = true;100 const loop = (now: number) => {101 raf = 0;102 if (!stage || !alive) return;103 const dt = last ? Math.min((now - last) / 1000, 0.05) : 1 / 60;104 last = now;105 if (!reduced) time += dt;106 const more = hooks.current.frame(stage, time, dt);107 stage.gl.drawArrays(stage.gl.TRIANGLES, 0, 3);108 if (more && !reduced && visible && !document.hidden) raf = requestAnimationFrame(loop);109 else last = 0;110 };111 const request = () => {112 if (!raf && alive && stage) raf = requestAnimationFrame(loop);113 };114 const resize = () => {115 if (!stage) return;116 const r = canvas.getBoundingClientRect();117 const dpr = Math.min(window.devicePixelRatio || 1, maxDpr);118 const w = Math.max(1, Math.round(r.width * dpr));119 const h = Math.max(1, Math.round(r.height * dpr));120 if (canvas.width !== w || canvas.height !== h) {121 canvas.width = w;122 canvas.height = h;123 }124 stage.w = w;125 stage.h = h;126 stage.dpr = dpr;127 stage.gl.viewport(0, 0, w, h);128 request();129 };130 const init = () => {131 stage = createStage(canvas, frag);132 if (!stage) {133 setFailed(true);134 return;135 }136 stage.reduced = reduced;137 hooks.current.setup?.(stage);138 resize();139 };140 const onLost = (e: Event) => {141 e.preventDefault();142 cancelAnimationFrame(raf);143 raf = 0;144 stage = null;145 };146 const onRestored = () => init();147 const onVis = () => !document.hidden && request();148 canvas.addEventListener("webglcontextlost", onLost);149 canvas.addEventListener("webglcontextrestored", onRestored);150 document.addEventListener("visibilitychange", onVis);151 const ro = new ResizeObserver(resize);152 ro.observe(canvas);153 const io = new IntersectionObserver(([e]) => {154 visible = e.isIntersecting;155 if (visible) request();156 });157 io.observe(canvas);158 kick.current = request;159 init();160 return () => {161 alive = false;162 cancelAnimationFrame(raf);163 ro.disconnect();164 io.disconnect();165 canvas.removeEventListener("webglcontextlost", onLost);166 canvas.removeEventListener("webglcontextrestored", onRestored);167 document.removeEventListener("visibilitychange", onVis);168 kick.current = () => {};169 };170 }, [ref, frag, hooks, maxDpr]);171 return { failed, kick };172}173 174function makeTexture(gl: GL, filter: number, wrap: number) {175 const t = gl.createTexture();176 gl.bindTexture(gl.TEXTURE_2D, t);177 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, filter);178 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, filter);179 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, wrap);180 gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, wrap);181 return t;182}183 184const hexToRgb = (hex: string): [number, number, number] => {185 let h = hex.replace("#", "").trim();186 if (h.length === 3) h = h.split("").map((c) => c + c).join("");187 const n = parseInt(h.slice(0, 6), 16) || 0;188 return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];189};190 191/* ───────────────────────── shader ───────────────────────── */192 193const FRAG = /* glsl */ `194uniform vec2 uRes;195uniform float uTime;196uniform vec4 uRip; // px x, px y, age, amp197uniform float uHover;198uniform float uTint;199 200vec2 hash2(vec2 p){201 p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));202 return -1. + 2. * fract(sin(p) * 43758.5453);203}204float noise(vec2 p){205 vec2 i = floor(p), f = fract(p);206 vec2 u = f * f * (3. - 2. * f);207 return mix(mix(dot(hash2(i), f), dot(hash2(i + vec2(1, 0)), f - vec2(1, 0)), u.x),208 mix(dot(hash2(i + vec2(0, 1)), f - vec2(0, 1)), dot(hash2(i + vec2(1, 1)), f - vec2(1, 1)), u.x), u.y);209}210float height(vec2 px){211 vec2 p = px / uRes.y * 1.6;212 float t = uTime * (.25 + uHover * .35);213 vec2 w = vec2(noise(p + vec2(t, 0.)), noise(p + vec2(3.1, -t)));214 float h = noise(p * 1.2 + w * 1.4) + .5 * sin(p.x * 1.4 - t * 1.6 + w.y * 3.);215 float d = length(px - uRip.xy) / uRes.y;216 float x = d - uRip.z * 3.2;217 h += uRip.w * sin(x * 18.) * exp(-x * x * 30.) * exp(-uRip.z * 2.2);218 return h;219}220vec3 studio(vec3 r){221 float el = r.y;222 vec3 sky = mix(vec3(.55, .57, .62), vec3(1.), smoothstep(-.1, .8, el));223 vec3 fl = mix(vec3(.03), vec3(.18, .17, .16), smoothstep(-1., -.3, el));224 vec3 c = mix(fl, sky, smoothstep(-.12, -.06, el));225 c += .6 * exp(-pow((el + .06) / .04, 2.));226 c += 1.1 * smoothstep(.1, 0., abs(r.x + .5) - .2) * smoothstep(.1, 0., abs(el - .4) - .2);227 return c;228}229void main(){230 vec2 px = gl_FragCoord.xy;231 float e = 1.5;232 float h = height(px);233 vec2 g = vec2(height(px + vec2(e, 0.)) - h, height(px + vec2(0., e)) - h) / e * uRes.y * .18;234 vec3 n = normalize(vec3(-g, 1.));235 // capsule geometry adds a convex bevel so the ring reads as a tube236 vec2 c = px - .5 * uRes;237 float R = .5 * uRes.y;238 vec2 q = vec2(max(abs(c.x) - (.5 * uRes.x - R), 0.), c.y);239 vec2 bevel = q / R;240 n = normalize(n + vec3(bevel * .9, 0.));241 vec3 v = vec3(0., 0., -1.);242 vec3 r = reflect(v, n);243 vec3 env = studio(r);244 vec3 tint = uTint < .5 ? vec3(.97) : uTint < 1.5 ? vec3(1., .8, .46) : uTint < 2.5 ? vec3(.8, .82, .86) : vec3(.5);245 vec3 col = env * tint;246 if (uTint > 2.5) col = pow(env, vec3(1.6)) * .6 + .02;247 col = col / (1. + col * .25) * 1.15;248 gl_FragColor = vec4(col, 1.);249}250`;251 252/* ───────────────────────── components ───────────────────────── */253 254type Tint = "silver" | "gold" | "titanium" | "black-chrome";255const TINTS: Record<Tint, number> = { silver: 0, gold: 1, titanium: 2, "black-chrome": 3 };256 257export interface LiquidMetalButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {258 variant?: "rim" | "solid";259 tint?: Tint;260 size?: "md" | "lg";261 icon?: ReactNode;262}263 264export const LiquidMetalButton = forwardRef<HTMLButtonElement, LiquidMetalButtonProps>(function LiquidMetalButton(265 { variant = "rim", tint = "silver", size = "lg", icon, className, style, children, type = "button", onPointerDown, onPointerEnter, onPointerLeave, ...rest },266 ref,267) {268 const canvasRef = useRef<HTMLCanvasElement>(null);269 const st = useRef({ t: 0, hover: 0, thover: 0, rip: { x: 0, y: 0, age: 9, amp: 0 }, tint });270 st.current.tint = tint;271 272 const hooks = useRef<StageHooks>({273 frame(s, _time, dt) {274 const { gl, u } = s;275 const S = st.current;276 S.hover += (S.thover - S.hover) * (1 - Math.exp(-dt * 6));277 if (!s.reduced) S.t += dt;278 S.rip.age += dt;279 gl.uniform2f(u.uRes, s.w, s.h);280 gl.uniform1f(u.uTime, S.t + 3);281 gl.uniform4f(u.uRip, S.rip.x * s.dpr, s.h - S.rip.y * s.dpr, S.rip.age, S.rip.age < 2 ? S.rip.amp : 0);282 gl.uniform1f(u.uHover, S.hover);283 gl.uniform1f(u.uTint, TINTS[S.tint] ?? 0);284 return true;285 },286 });287 const { failed, kick } = useStage(canvasRef, FRAG, hooks);288 useEffect(() => kick.current(), [tint, kick]);289 290 if (failed) {291 return (292 <MetalButtonCSS ref={ref} variant={variant} tint={tint} size={size} icon={icon} className={className} style={style} type={type} onPointerDown={onPointerDown} onPointerEnter={onPointerEnter} onPointerLeave={onPointerLeave} {...rest}>293 {children}294 </MetalButtonCSS>295 );296 }297 298 return (299 <button300 ref={ref}301 type={type}302 className={`fd-lmbtn${className ? " " + className : ""}`}303 data-variant={variant}304 data-size={size}305 data-tint={tint}306 style={style}307 onPointerDown={(e) => {308 const r = e.currentTarget.getBoundingClientRect();309 st.current.rip = { x: e.clientX - r.left, y: e.clientY - r.top, age: 0, amp: window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ? 0 : 0.9 };310 kick.current();311 onPointerDown?.(e);312 }}313 onPointerEnter={(e) => {314 st.current.thover = 1;315 onPointerEnter?.(e);316 }}317 onPointerLeave={(e) => {318 st.current.thover = 0;319 onPointerLeave?.(e);320 }}321 {...rest}322 >323 <style>{css}</style>324 <canvas ref={canvasRef} className="fd-lmbtn-metal" aria-hidden="true" />325 <span className="fd-lmbtn-face" aria-hidden="true" />326 <span className="fd-lmbtn-label">327 {children}328 {icon ? <span className="fd-lmbtn-icon">{icon}</span> : null}329 </span>330 </button>331 );332});333 334/** CSS-only chrome capsule: conic + linear gradients and a specular sweep. No JS, no WebGL. */335export const MetalButtonCSS = forwardRef<HTMLButtonElement, LiquidMetalButtonProps>(function MetalButtonCSS(336 { variant = "rim", tint = "silver", size = "lg", icon, className, children, type = "button", ...rest },337 ref,338) {339 return (340 <button ref={ref} type={type} className={`fd-lmbtn fd-lmbtn-css${className ? " " + className : ""}`} data-variant={variant} data-size={size} data-tint={tint} {...rest}>341 <style>{css}</style>342 <span className="fd-lmbtn-chrome" aria-hidden="true" />343 <span className="fd-lmbtn-face" aria-hidden="true" />344 <span className="fd-lmbtn-label">345 {children}346 {icon ? <span className="fd-lmbtn-icon">{icon}</span> : null}347 </span>348 </button>349 );350});351 352const css = `353.fd-lmbtn, .fd-lmbtn *, .fd-lmbtn *::before, .fd-lmbtn *::after { box-sizing: border-box; }354.fd-lmbtn {355 --fd-lmbtn-ring: 3px;356 --fd-lmbtn-face: #0E0E11;357 --fd-lmbtn-ink: #F5F5F7;358 --fd-lmbtn-h: 56px;359 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);360 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);361 appearance: none; -webkit-appearance: none;362 position: relative; display: inline-flex; align-items: center; justify-content: center;363 height: var(--fd-lmbtn-h); margin: 0; padding: 0 32px; border: 0; border-radius: 9999px;364 background: #5c5f66; color: var(--fd-lmbtn-ink); cursor: pointer; isolation: isolate; overflow: hidden;365 font: 600 1.0625rem/1 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;366 letter-spacing: -0.01em; text-decoration: none; text-transform: none; white-space: nowrap;367 -webkit-tap-highlight-color: transparent; touch-action: manipulation;368 box-shadow: 0 1px 1px rgba(0,0,0,.25), 0 10px 30px -10px rgba(0,0,0,.55);369 transition: transform 120ms var(--fd-ease-out), box-shadow 240ms var(--fd-ease-out);370}371.fd-lmbtn[data-size="md"] { --fd-lmbtn-h: 44px; padding: 0 22px; font-size: 0.9375rem; --fd-lmbtn-ring: 2px; }372.fd-lmbtn:hover { box-shadow: 0 1px 1px rgba(0,0,0,.25), 0 16px 40px -12px rgba(0,0,0,.6); }373.fd-lmbtn:active { transform: scale(0.96); }374.fd-lmbtn:not(:active) { transition-timing-function: var(--fd-spring), var(--fd-ease-out); }375.fd-lmbtn:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 2px; }376.fd-lmbtn:disabled { cursor: not-allowed; opacity: .5; }377.fd-lmbtn-metal, .fd-lmbtn-chrome { position: absolute; inset: 0; display: block; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; border-radius: inherit; z-index: 0; }378.fd-lmbtn-face {379 position: absolute; inset: var(--fd-lmbtn-ring); display: block; margin: 0; padding: 0; border: 0;380 border-radius: 9999px; z-index: 1; background: var(--fd-lmbtn-face);381 box-shadow: inset 0 1px 0 rgba(255,255,255,.08), inset 0 -8px 16px rgba(0,0,0,.35);382}383.fd-lmbtn[data-variant="solid"] { --fd-lmbtn-ink: #101114; }384.fd-lmbtn[data-variant="solid"] .fd-lmbtn-face {385 inset: 0; background: linear-gradient(180deg, rgba(255,255,255,.62), rgba(236,238,242,.42) 50%, rgba(255,255,255,.55));386 box-shadow: inset 0 0 0 1px rgba(255,255,255,.4), inset 0 1px 0 rgba(255,255,255,.85), inset 0 -1px 0 rgba(0,0,0,.15);387}388.fd-lmbtn[data-variant="solid"][data-tint="gold"] .fd-lmbtn-face { background: linear-gradient(180deg, rgba(255,246,222,.6), rgba(240,214,150,.38) 50%, rgba(255,240,200,.55)); }389.fd-lmbtn[data-variant="solid"][data-tint="black-chrome"] .fd-lmbtn-face { background: linear-gradient(180deg, rgba(20,21,24,.55), rgba(8,8,10,.4) 50%, rgba(20,21,24,.6)); box-shadow: inset 0 0 0 1px rgba(255,255,255,.12), inset 0 1px 0 rgba(255,255,255,.25); }390.fd-lmbtn[data-variant="solid"] .fd-lmbtn-label { text-shadow: 0 1px 0 rgba(255,255,255,.55); }391.fd-lmbtn[data-variant="solid"][data-tint="black-chrome"] { --fd-lmbtn-ink: #F5F5F7; }392.fd-lmbtn[data-variant="solid"][data-tint="black-chrome"] .fd-lmbtn-label { text-shadow: 0 1px 1px rgba(0,0,0,.6); }393.fd-lmbtn-label { position: relative; z-index: 2; display: inline-flex; align-items: center; gap: 10px; margin: 0; padding: 0; color: var(--fd-lmbtn-ink); font: inherit; letter-spacing: inherit; }394.fd-lmbtn-icon { display: inline-flex; margin: 0; padding: 0; transition: transform 240ms var(--fd-ease-out); }395.fd-lmbtn-icon svg { display: block; }396.fd-lmbtn:hover .fd-lmbtn-icon { transform: translateX(3px); }397 398/* CSS chrome */399.fd-lmbtn-chrome {400 background:401 linear-gradient(180deg, rgba(255,255,255,.0) 0 46%, rgba(0,0,0,.55) 50%, rgba(255,255,255,.0) 64%),402 conic-gradient(from 200deg at 50% 50%, #f9fafb, #8a8e96 12%, #f4f5f7 22%, #2a2b2f 34%, #c9ccd2 46%, #fdfdfd 58%, #61646b 70%, #e7e9ec 82%, #3a3c41 92%, #f9fafb);403}404.fd-lmbtn-css[data-tint="gold"] .fd-lmbtn-chrome { background:405 linear-gradient(180deg, rgba(255,255,255,0) 0 46%, rgba(40,24,0,.5) 50%, rgba(255,255,255,0) 64%),406 conic-gradient(from 200deg at 50% 50%, #fff4d6, #a87a33 12%, #fff1c9 22%, #3d2a0f 34%, #e2bd72 46%, #fff8e6 58%, #80602a 70%, #f5d996 82%, #4d3712 92%, #fff4d6); }407.fd-lmbtn-css[data-tint="titanium"] .fd-lmbtn-chrome { background:408 linear-gradient(180deg, rgba(255,255,255,0) 0 46%, rgba(0,0,0,.5) 50%, rgba(255,255,255,0) 64%),409 conic-gradient(from 200deg at 50% 50%, #e9ecf0, #737a85 12%, #e1e5ea 22%, #23262c 34%, #aeb5bf 46%, #f3f5f8 58%, #525862 70%, #d6dbe2 82%, #30343a 92%, #e9ecf0); }410.fd-lmbtn-css[data-tint="black-chrome"] .fd-lmbtn-chrome { background:411 linear-gradient(180deg, rgba(255,255,255,0) 0 46%, rgba(0,0,0,.6) 50%, rgba(255,255,255,0) 64%),412 conic-gradient(from 200deg at 50% 50%, #7d8087, #121316 12%, #6a6d74 22%, #040405 34%, #3f4248 46%, #9a9da4 58%, #17181b 70%, #55585e 82%, #08090a 92%, #7d8087); }413.fd-lmbtn-chrome::after {414 content: ""; position: absolute; inset: 0; border-radius: inherit;415 background: linear-gradient(105deg, transparent 0 35%, rgba(255,255,255,.85) 48%, transparent 60%);416 background-size: 260% 100%; background-position: 120% 0; mix-blend-mode: overlay;417 animation: fd-lmbtn-sweep 3.6s cubic-bezier(0.65, 0, 0.35, 1) infinite;418}419@keyframes fd-lmbtn-sweep { 0%, 30% { background-position: 120% 0; } 70%, 100% { background-position: -20% 0; } }420@media (prefers-reduced-motion: reduce) {421 .fd-lmbtn-chrome::after { animation: none; }422 .fd-lmbtn, .fd-lmbtn-icon { transition: none; }423}424@media (prefers-contrast: more) { .fd-lmbtn-face { box-shadow: inset 0 0 0 1px #fff; } }425`;426 427/* ───────────────────────── demo ───────────────────────── */428 429const Arrow = (430 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">431 <path d="M3 8h9M8.5 4l4 4-4 4" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" />432 </svg>433);434 435export function Demo() {436 const [plan, setPlan] = useState<"yearly" | "monthly">("yearly");437 return (438 <section className="fd-lmbtn-demo">439 <style>{demoCss}</style>440 <div className="fd-lmbtn-demo-card" aria-hidden="true">441 <div className="fd-lmbtn-demo-card-top">442 <span className="fd-lmbtn-demo-card-brand">Northvale</span>443 <span className="fd-lmbtn-demo-card-tier">METAL</span>444 </div>445 <div className="fd-lmbtn-demo-chip" />446 <div className="fd-lmbtn-demo-card-num">•••• •••• •••• 4821</div>447 <div className="fd-lmbtn-demo-card-bottom">448 <span>ELIF DEMIR</span>449 <span>09/31</span>450 </div>451 </div>452 <div className="fd-lmbtn-demo-panel">453 <p className="fd-lmbtn-demo-kicker">Northvale Metal</p>454 <h2 className="fd-lmbtn-demo-title">18 grams of stainless steel. 3% back on everything.</h2>455 <ul className="fd-lmbtn-demo-perks">456 <li>3% cashback, no category caps</li>457 <li>Airport lounges in 1,400 cities</li>458 <li>0% FX on 150+ currencies</li>459 </ul>460 <div className="fd-lmbtn-demo-toggle" role="radiogroup" aria-label="Billing">461 <button type="button" role="radio" aria-checked={plan === "yearly"} onClick={() => setPlan("yearly")}>Yearly · €16.60/mo</button>462 <button type="button" role="radio" aria-checked={plan === "monthly"} onClick={() => setPlan("monthly")}>Monthly · €19.90</button>463 </div>464 <div className="fd-lmbtn-demo-actions">465 <LiquidMetalButton icon={Arrow}>Upgrade to Metal</LiquidMetalButton>466 <LiquidMetalButton variant="solid" size="md">Compare plans</LiquidMetalButton>467 </div>468 <div className="fd-lmbtn-demo-alt">469 <span className="fd-lmbtn-demo-alt-label">CSS-only fallback</span>470 <MetalButtonCSS size="md" icon={Arrow}>Upgrade</MetalButtonCSS>471 <MetalButtonCSS size="md" variant="solid" tint="gold">Gold</MetalButtonCSS>472 <MetalButtonCSS size="md" variant="solid" tint="black-chrome">Black</MetalButtonCSS>473 </div>474 <p className="fd-lmbtn-demo-fine">Billed €199 yearly. Cancel anytime in the app. Card ships in 5–7 days.</p>475 </div>476 </section>477 );478}479 480const demoCss = `481.fd-lmbtn-demo, .fd-lmbtn-demo *, .fd-lmbtn-demo *::before, .fd-lmbtn-demo *::after { box-sizing: border-box; }482.fd-lmbtn-demo {483 min-height: 100vh; margin: 0; padding: clamp(32px, 6vw, 80px) clamp(16px, 4vw, 48px);484 display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); align-items: center; gap: clamp(32px, 6vw, 96px);485 background: radial-gradient(900px 600px at 25% 40%, #23242A 0%, #0B0B0F 70%); color: #F5F5F7;486 font: 400 15px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;487}488.fd-lmbtn-demo-card {489 justify-self: end; width: min(100%, 420px); aspect-ratio: 1.586; margin: 0; padding: 26px 28px; border-radius: 20px;490 display: flex; flex-direction: column; justify-content: space-between; color: #1A1B1F;491 background:492 repeating-linear-gradient(90deg, rgba(255,255,255,.06) 0 1px, transparent 1px 3px),493 linear-gradient(125deg, #f2f3f5 0%, #a9adb4 28%, #eef0f2 46%, #6e727a 64%, #d9dce0 82%, #9a9ea6 100%);494 box-shadow: inset 0 1px 0 rgba(255,255,255,.8), inset 0 -1px 0 rgba(0,0,0,.25), 0 40px 80px -30px rgba(0,0,0,.8), 0 0 0 1px rgba(0,0,0,.4);495 transform: perspective(1200px) rotateY(14deg) rotateX(6deg) rotateZ(-3deg);496}497.fd-lmbtn-demo-card-top { display: flex; justify-content: space-between; align-items: center; margin: 0; padding: 0; }498.fd-lmbtn-demo-card-brand { margin: 0; padding: 0; font: 700 20px/1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", sans-serif; letter-spacing: -0.03em; color: #1A1B1F; }499.fd-lmbtn-demo-card-tier { margin: 0; padding: 0; font: 600 11px/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: .2em; color: #3A3C42; }500.fd-lmbtn-demo-chip { width: 46px; height: 34px; margin: 0; padding: 0; border-radius: 7px; background: linear-gradient(135deg, #e8d29a, #b0904a 50%, #f1e0b0); box-shadow: inset 0 0 0 1px rgba(0,0,0,.2); }501.fd-lmbtn-demo-card-num { margin: 0; padding: 0; font: 500 19px/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: .08em; color: #23252A; text-shadow: 0 1px 0 rgba(255,255,255,.5); }502.fd-lmbtn-demo-card-bottom { display: flex; justify-content: space-between; margin: 0; padding: 0; font: 600 12px/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: .12em; color: #2A2C31; }503.fd-lmbtn-demo-panel { max-width: 480px; margin: 0; padding: 0; }504.fd-lmbtn-demo-kicker { margin: 0 0 12px; padding: 0; color: #A1A1A6; font: 600 13px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; }505.fd-lmbtn-demo-title { margin: 0 0 20px; padding: 0; color: #F5F5F7; font: 700 clamp(2rem, 3.6vw, 2.75rem)/1.05 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.03em; }506.fd-lmbtn-demo-perks { margin: 0 0 24px; padding: 0; list-style: none; }507.fd-lmbtn-demo-perks li { position: relative; margin: 0; padding: 7px 0 7px 26px; color: #C7C7CC; font: 400 15px/1.4 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; list-style: none; }508.fd-lmbtn-demo-perks li::before { content: ""; position: absolute; left: 2px; top: 12px; width: 12px; height: 7px; border-left: 2px solid #D2D4D9; border-bottom: 2px solid #D2D4D9; transform: rotate(-45deg); }509.fd-lmbtn-demo-toggle { display: inline-flex; margin: 0 0 28px; padding: 4px; border-radius: 9999px; background: #1C1C22; box-shadow: inset 0 0 0 1px rgba(255,255,255,.06); }510.fd-lmbtn-demo-toggle button {511 appearance: none; -webkit-appearance: none; margin: 0; padding: 9px 16px; border: 0; border-radius: 9999px; background: transparent; color: #8E8E93; cursor: pointer;512 font: 500 13px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0;513 transition: background 240ms cubic-bezier(0.65, 0, 0.35, 1), color 240ms cubic-bezier(0.65, 0, 0.35, 1);514}515.fd-lmbtn-demo-toggle button[aria-checked="true"] { background: #3A3A40; color: #F5F5F7; }516.fd-lmbtn-demo-toggle button:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 2px; }517.fd-lmbtn-demo-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 14px; margin: 0 0 28px; padding: 0; }518.fd-lmbtn-demo-alt { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin: 0 0 20px; padding: 18px 0 0; border-top: 1px solid #2A2A2E; }519.fd-lmbtn-demo-alt-label { width: 100%; margin: 0 0 2px; padding: 0; color: #6B6B76; font: 500 12px/1.2 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: .04em; }520.fd-lmbtn-demo-fine { margin: 0; padding: 0; color: #6B6B76; font: 400 13px/1.4 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; }521@media (max-width: 860px) {522 .fd-lmbtn-demo { grid-template-columns: 1fr; }523 .fd-lmbtn-demo-card { justify-self: center; transform: none; width: min(100%, 360px); }524}525`;Flowing chrome: a warped height field reflecting a procedural studio, in silver, gold, titanium, black chrome or oil-slick.
A giant headline cast in flowing chrome with bevelled edges; tilt your pointer and the studio lights slide across the letters.
A raymarched drop of mercury that morphs, merges and reaches for your cursor — a hero centrepiece on a transparent canvas.