Liquid Metal Button
A capsule CTA with a live chrome rim that ripples from your touch — plus a zero-JS CSS chrome variant.
Flowing chrome: a warped height field reflecting a procedural studio, in silver, gold, titanium, black chrome or oil-slick.
Browser support · WebGL2 / WebGL1. Without WebGL an animated CSS chrome gradient is shown.
npx shadcn@latest add https://freedesignmd.com/r/liquid-metal.json1/**2 * LiquidMetal — a flowing chrome surface. Domain-warped noise drives a height3 * field; its normals reflect a procedural studio environment (soft boxes,4 * horizon line, floor bounce) with Fresnel and an optional thin-film5 * iridescence. The cursor bulges the surface and taps send out ripples.6 *7 * Props8 * - tint?: "silver" | "gold" | "titanium" | "black-chrome" | "iridescent". Default "silver".9 * - speed?: number Flow speed multiplier. Default 1.10 * - distortion?: number Warp amount 0–2. Default 1.11 * - scale?: number Feature size; larger = broader pools. Default 1.12 * - interactive?: boolean Cursor bulge + tap ripples. Default true.13 * - quality?: number Max render DPR (metal is smooth; 1.5 looks identical to 2 at ~half the cost). Default 1.5.14 * - children Content rendered above the surface.15 * CSS vars: --fd-lmetal-radius, --fd-lmetal-fallback.16 *17 * Browser support: WebGL2 / WebGL1. Without WebGL an animated CSS chrome18 * gradient is shown. Reduced motion: a single still frame; no ripples.19 * The canvas is aria-hidden.20 */21import { useEffect, useRef, useState } from "react";22import type { 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 float uDist;197uniform float uScale;198uniform vec3 uPtr; // px x, px y, strength199uniform vec4 uRip[4]; // px x, px y, age (s), amplitude200uniform float uTint;201uniform float uDpr;202 203vec2 hash2(vec2 p){204 p = vec2(dot(p, vec2(127.1, 311.7)), dot(p, vec2(269.5, 183.3)));205 return -1. + 2. * fract(sin(p) * 43758.5453);206}207float noise(vec2 p){208 vec2 i = floor(p), f = fract(p);209 vec2 u = f * f * (3. - 2. * f);210 return mix(mix(dot(hash2(i), f), dot(hash2(i + vec2(1, 0)), f - vec2(1, 0)), u.x),211 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);212}213 214float height(vec2 p, vec2 px){215 float t = uTime * .11;216 // two-octave warp field217 vec2 w = vec2(noise(p * .7 + vec2(t, -t * .7)) + .5 * noise(p * 1.4 - t),218 noise(p * .7 + vec2(-t * .8, t) + 5.2) + .5 * noise(p * 1.4 + t + 3.1));219 vec2 q = p + uDist * 1.1 * w;220 float h = noise(q * .9 + t * .4) + .4 * noise(q * 1.9 - t * .3) + .14 * noise(q * 4.1 + t * .2);221 // long liquid bands, the signature of poured chrome222 h += .45 * sin(q.x * .9 + q.y * .45 + h * 2.2 + t * 2.);223 224 // cursor bulge225 vec2 dp = (px - uPtr.xy) / uDpr;226 float d2 = dot(dp, dp);227 h += uPtr.z * .9 * exp(-d2 / 9000.);228 // tap ripples229 for (int i = 0; i < 4; i++) {230 vec4 r = uRip[i];231 if (r.w <= 0.) continue;232 float d = length(px - r.xy) / uDpr;233 float front = r.z * 420.;234 float x = d - front;235 h += r.w * sin(x * .06) * exp(-x * x / 5200.) * exp(-r.z * 1.4);236 }237 return h;238}239 240vec3 studio(vec3 r){241 // r: reflected direction, z toward viewer, y up242 float el = r.y;243 float az = atan(r.x, r.z);244 // the wall behind the camera: graded charcoal → pale, so flat metal reads moody, not grey245 vec3 sky = mix(vec3(.3, .31, .34), vec3(.92, .93, .96), smoothstep(0., .7, el + .3 * abs(az)));246 vec3 floorC = mix(vec3(.02, .021, .025), vec3(.14, .13, .12), smoothstep(-1., -.3, el));247 vec3 c = mix(floorC, sky, smoothstep(-.12, -.06, el));248 // bright cyc sweep right above the horizon249 c += vec3(1.) * .7 * exp(-pow((el + .04) / .03, 2.));250 // soft boxes: a big key left, a strip right, a top light251 float b1 = smoothstep(.08, 0., abs(az + .55) - .2) * smoothstep(.06, 0., abs(el - .35) - .22);252 float b2 = smoothstep(.04, 0., abs(az - .62) - .07) * smoothstep(.05, 0., abs(el - .18) - .3);253 float b3 = smoothstep(.15, 0., abs(el - .9) - .05);254 c += vec3(1.5) * b1 + vec3(1.2) * b2 + vec3(.7) * b3;255 // dark flags give chrome its deep contrast bands256 c *= 1. - .85 * smoothstep(.06, 0., abs(az - .3) - .05) * smoothstep(-.05, .1, el);257 c *= 1. - .6 * smoothstep(.06, 0., abs(az + 1.25) - .1) * smoothstep(-.05, .1, el);258 return c;259}260 261vec3 tintOf(float k, out float black){262 black = 0.;263 if (k < .5) return vec3(.96, .965, .975); // silver264 if (k < 1.5) return vec3(1., .78, .42); // gold265 if (k < 2.5) return vec3(.78, .8, .84); // titanium266 if (k < 3.5) { black = 1.; return vec3(.55, .56, .6); } // black chrome267 return vec3(.95); // iridescent base268}269 270void main(){271 vec2 px = gl_FragCoord.xy;272 float m = min(uRes.x, uRes.y);273 vec2 p = (px - .5 * uRes) / m * 1.7 / uScale;274 float e = 1.5 / m;275 float h = height(p, px);276 float hx = height(p + vec2(e, 0.), px + vec2(1.5, 0.));277 float hy = height(p + vec2(0., e), px + vec2(0., 1.5));278 vec3 n = normalize(vec3(-(hx - h) / e, -(hy - h) / e, 1.) * vec3(.26, .26, 1.));279 280 // slight perspective so the top of the frame "looks" further away281 vec2 sp = (px / uRes - .5);282 vec3 v = normalize(vec3(sp.x * .35, sp.y * .35 + .1, -1.));283 vec3 r = reflect(v, n);284 vec3 env = studio(r);285 286 float black;287 vec3 tint = tintOf(uTint, black);288 float cosT = clamp(dot(n, -v), 0., 1.);289 float fres = pow(1. - cosT, 5.);290 vec3 F = mix(tint, vec3(1.), fres);291 vec3 col = env * F;292 293 if (black > .5) col = pow(env, vec3(1.7)) * .55 * F + .02;294 if (uTint > 3.5) {295 // thin film: thickness from height + view angle296 float film = h * .55 + cosT * 1.2 + uTime * .03;297 vec3 iri = .62 + .38 * cos(6.2831 * (film + vec3(0., .33, .67)));298 // film shows most on mid-tones; highlights stay white, shadows stay deep299 float lum = dot(col, vec3(.333));300 col *= mix(vec3(1.), iri * 1.12, .42 * smoothstep(.05, .5, lum) * (1. - .5 * smoothstep(.9, 1.4, lum)));301 }302 if (uTint > .5 && uTint < 1.5) col += vec3(.06, .03, 0.) * fres;303 304 // gentle filmic shoulder + vignette305 col = col / (1. + col * .28) * 1.18;306 float vig = 1. - .32 * dot(sp, sp);307 col *= vig;308 gl_FragColor = vec4(col, 1.);309}310`;311 312/* ───────────────────────── component ───────────────────────── */313 314export type MetalTint = "silver" | "gold" | "titanium" | "black-chrome" | "iridescent";315const TINTS: Record<MetalTint, number> = { silver: 0, gold: 1, titanium: 2, "black-chrome": 3, iridescent: 4 };316const FALLBACKS: Record<MetalTint, string> = {317 silver: "linear-gradient(160deg, #f4f5f7 0%, #9da1a8 22%, #fdfdfd 38%, #2b2c30 52%, #c7cad0 66%, #5d6067 84%, #eef0f3 100%)",318 gold: "linear-gradient(160deg, #fff1cc 0%, #b98a3e 22%, #fff6dc 38%, #3a2a12 52%, #e5c27a 66%, #7a5a26 84%, #fbe6b3 100%)",319 titanium: "linear-gradient(160deg, #e6e9ee 0%, #868d98 22%, #f1f3f6 38%, #24272d 52%, #b3b9c2 66%, #535963 84%, #dfe3e9 100%)",320 "black-chrome": "linear-gradient(160deg, #6b6e75 0%, #1b1c20 22%, #9a9da4 38%, #050506 52%, #45484e 66%, #0f1013 84%, #7a7d84 100%)",321 iridescent: "linear-gradient(160deg, #f3e9ff 0%, #8fb7d8 22%, #fff5e1 38%, #2e2a3a 52%, #d7c1f0 66%, #5d7f80 84%, #f2f0e6 100%)",322};323 324export interface LiquidMetalProps {325 tint?: MetalTint;326 speed?: number;327 distortion?: number;328 scale?: number;329 interactive?: boolean;330 quality?: number;331 className?: string;332 style?: CSSProperties;333 children?: ReactNode;334}335 336export function LiquidMetal({337 tint = "silver",338 speed = 1,339 distortion = 1,340 scale = 1,341 interactive = true,342 quality = 1.5,343 className,344 style,345 children,346}: LiquidMetalProps) {347 const rootRef = useRef<HTMLDivElement>(null);348 const canvasRef = useRef<HTMLCanvasElement>(null);349 const props = useRef({ tint, speed, distortion, scale, interactive });350 props.current = { tint, speed, distortion, scale, interactive };351 const st = useRef({352 t: 0,353 ptr: { x: 0, y: 0, tx: 0, ty: 0, s: 0, ts: 0 },354 rip: [] as { x: number; y: number; age: number; amp: number }[],355 tint: TINTS[tint],356 });357 358 const hooks = useRef<StageHooks>({359 frame(s, _time, dt) {360 const { gl, u } = s;361 const p = props.current;362 const S = st.current;363 if (!s.reduced) S.t += dt * p.speed;364 const pt = S.ptr;365 const k = s.reduced ? 1 : 1 - Math.exp(-dt * 6);366 pt.x += (pt.tx - pt.x) * k;367 pt.y += (pt.ty - pt.y) * (1 - Math.exp(-dt * 6));368 pt.s += (pt.ts - pt.s) * (1 - Math.exp(-dt * 3));369 S.rip = S.rip.filter((r) => (r.age += dt) < 2.6);370 const rip = new Float32Array(16);371 S.rip.slice(-4).forEach((r, i) => rip.set([r.x * s.dpr, s.h - r.y * s.dpr, r.age, r.amp], i * 4));372 gl.uniform2f(u.uRes, s.w, s.h);373 gl.uniform1f(u.uTime, S.t + 7);374 gl.uniform1f(u.uDist, p.distortion);375 gl.uniform1f(u.uScale, p.scale);376 gl.uniform3f(u.uPtr, pt.x * s.dpr, s.h - pt.y * s.dpr, p.interactive ? pt.s : 0);377 gl.uniform4fv(u.uRip, rip);378 gl.uniform1f(u.uTint, TINTS[p.tint] ?? 0);379 gl.uniform1f(u.uDpr, s.dpr);380 return true;381 },382 });383 const { failed, kick } = useStage(canvasRef, FRAG, hooks, quality);384 useEffect(() => kick.current(), [tint, distortion, scale, kick]);385 386 useEffect(() => {387 const el = rootRef.current;388 if (!el || !interactive) return;389 const reduced = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches ?? false;390 if (reduced) return;391 let lastX = 0;392 let lastY = 0;393 let travel = 0;394 const local = (e: PointerEvent) => {395 const r = el.getBoundingClientRect();396 return [e.clientX - r.left, e.clientY - r.top];397 };398 const move = (e: PointerEvent) => {399 const [x, y] = local(e);400 const pt = st.current.ptr;401 if (pt.ts === 0) {402 pt.x = x;403 pt.y = y;404 lastX = x;405 lastY = y;406 }407 pt.tx = x;408 pt.ty = y;409 pt.ts = 1;410 travel += Math.hypot(x - lastX, y - lastY);411 lastX = x;412 lastY = y;413 if (travel > 260) {414 travel = 0;415 st.current.rip.push({ x, y, age: 0, amp: 0.22 });416 }417 };418 const down = (e: PointerEvent) => {419 const [x, y] = local(e);420 st.current.rip.push({ x, y, age: 0, amp: 0.6 });421 };422 const leave = () => {423 st.current.ptr.ts = 0;424 };425 el.addEventListener("pointermove", move);426 el.addEventListener("pointerdown", down);427 el.addEventListener("pointerleave", leave);428 return () => {429 el.removeEventListener("pointermove", move);430 el.removeEventListener("pointerdown", down);431 el.removeEventListener("pointerleave", leave);432 };433 }, [interactive]);434 435 return (436 <div ref={rootRef} className={`fd-lmetal${className ? " " + className : ""}`} style={{ ["--fd-lmetal-fallback" as string]: FALLBACKS[tint], ...style }}>437 <style>{css}</style>438 {failed ? <div className="fd-lmetal-fallback" aria-hidden="true" /> : <canvas ref={canvasRef} className="fd-lmetal-canvas" aria-hidden="true" />}439 {children ? <div className="fd-lmetal-content">{children}</div> : null}440 </div>441 );442}443 444const css = `445.fd-lmetal, .fd-lmetal *, .fd-lmetal *::before, .fd-lmetal *::after { box-sizing: border-box; }446.fd-lmetal {447 position: relative; display: block; margin: 0; padding: 0; border: 0; overflow: hidden;448 border-radius: var(--fd-lmetal-radius, 0px); background: #8d9096; color: inherit; isolation: isolate;449}450.fd-lmetal-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; z-index: 0; }451.fd-lmetal-fallback {452 position: absolute; inset: -20%; margin: 0; padding: 0; border: 0; z-index: 0;453 background: var(--fd-lmetal-fallback); background-size: 160% 160%;454 filter: blur(18px) saturate(1.1);455 animation: fd-lmetal-flow 14s cubic-bezier(0.65, 0, 0.35, 1) infinite alternate;456}457@keyframes fd-lmetal-flow { from { background-position: 0% 0%; } to { background-position: 100% 100%; } }458@media (prefers-reduced-motion: reduce) { .fd-lmetal-fallback { animation: none; } }459.fd-lmetal-content { position: relative; z-index: 1; margin: 0; padding: 0; height: 100%; }460`;461 462/* ───────────────────────── demo ───────────────────────── */463 464const FINISHES: { id: MetalTint; name: string; price: string; swatch: string }[] = [465 { id: "silver", name: "Polished steel", price: "CHF 12,400", swatch: "linear-gradient(135deg,#fafafa,#8e9299 55%,#e9eaec)" },466 { id: "titanium", name: "Grade 5 titanium", price: "CHF 13,900", swatch: "linear-gradient(135deg,#e3e6eb,#6d737d 55%,#c9ced6)" },467 { id: "gold", name: "18k yellow gold", price: "CHF 38,500", swatch: "linear-gradient(135deg,#fff0c4,#b0853a 55%,#f3d891)" },468 { id: "black-chrome", name: "Black DLC", price: "CHF 14,800", swatch: "linear-gradient(135deg,#7b7e85,#0d0e10 55%,#4a4d53)" },469 { id: "iridescent", name: "Oil-slick PVD", price: "CHF 16,200", swatch: "linear-gradient(135deg,#f4e3ff,#6f8fb3 45%,#e7d39a 75%,#b9e2d4)" },470];471 472export function Demo() {473 const [finish, setFinish] = useState<MetalTint>("silver");474 const current = FINISHES.find((f) => f.id === finish)!;475 return (476 <section className="fd-lmetal-demo">477 <style>{demoCss}</style>478 <header className="fd-lmetal-demo-head">479 <p className="fd-lmetal-demo-kicker">Halden Chronométrie · Ref. 41 Mercure</p>480 <h1 className="fd-lmetal-demo-title">Cast in liquid light.</h1>481 <p className="fd-lmetal-demo-sub">A 41 mm monobloc case, hand-polished for 31 hours until it reads like poured metal. Limited to 410 pieces worldwide.</p>482 </header>483 <LiquidMetal className="fd-lmetal-demo-panel" tint={finish}>484 <div className="fd-lmetal-demo-bar" role="group" aria-label="Case finish">485 <div className="fd-lmetal-demo-swatches">486 {FINISHES.map((f) => (487 <button488 key={f.id}489 type="button"490 className="fd-lmetal-demo-swatch"491 aria-pressed={finish === f.id}492 aria-label={f.name}493 title={f.name}494 onClick={() => setFinish(f.id)}495 style={{ background: f.swatch }}496 />497 ))}498 </div>499 <div className="fd-lmetal-demo-info">500 <span className="fd-lmetal-demo-finish">{current.name}</span>501 <span className="fd-lmetal-demo-price">{current.price}</span>502 </div>503 <a className="fd-lmetal-demo-cta" href="#reserve" onClick={(e) => e.preventDefault()}>Reserve</a>504 </div>505 </LiquidMetal>506 <dl className="fd-lmetal-demo-specs">507 <div><dt>Movement</dt><dd>HC-07 manual, 72 h</dd></div>508 <div><dt>Water resistance</dt><dd>100 m</dd></div>509 <div><dt>Thickness</dt><dd>9.8 mm</dd></div>510 <div><dt>Deliveries</dt><dd>From March 2027</dd></div>511 </dl>512 </section>513 );514}515 516const demoCss = `517.fd-lmetal-demo, .fd-lmetal-demo *, .fd-lmetal-demo *::before, .fd-lmetal-demo *::after { box-sizing: border-box; }518.fd-lmetal-demo {519 --fd-ease-out: cubic-bezier(0.22, 1, 0.36, 1);520 --fd-spring: cubic-bezier(0.34, 1.56, 0.64, 1);521 min-height: 100vh; margin: 0; padding: clamp(48px, 9vh, 104px) clamp(16px, 4vw, 48px) 48px;522 display: flex; flex-direction: column; align-items: center; gap: clamp(32px, 5vh, 56px);523 background: #000; color: #F5F5F7;524 font: 400 15px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;525}526.fd-lmetal-demo-head { max-width: 760px; margin: 0; padding: 0; text-align: center; }527.fd-lmetal-demo-kicker { margin: 0 0 14px; padding: 0; color: #A1A1A6; font: 500 13px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; }528.fd-lmetal-demo-title {529 margin: 0 0 18px; padding: 0; letter-spacing: -0.045em;530 font: 700 clamp(2.75rem, 7vw, 5.5rem)/1 "SF Pro Display", ui-sans-serif, -apple-system, "Inter", "Segoe UI", Roboto, sans-serif;531 color: #F5F5F7;532 background: linear-gradient(180deg, #FFFFFF 0%, #D2D3D8 55%, #8E9097 100%); -webkit-background-clip: text; background-clip: text; -webkit-text-fill-color: transparent;533}534.fd-lmetal-demo-sub { max-width: 52ch; margin: 0 auto; padding: 0; color: #A1A1A6; font: 400 1.0625rem/1.5 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em; }535.fd-lmetal-demo-panel.fd-lmetal { width: 100%; max-width: 1280px; aspect-ratio: 16 / 8; min-height: 380px; --fd-lmetal-radius: 28px; cursor: grab; }536.fd-lmetal-demo-panel:active { cursor: grabbing; }537.fd-lmetal-demo-panel.fd-lmetal .fd-lmetal-content { display: flex; align-items: flex-end; justify-content: center; padding: 24px; }538.fd-lmetal-demo-bar {539 display: flex; align-items: center; gap: 18px; margin: 0; padding: 8px 8px 8px 14px;540 border: 1px solid rgba(255,255,255,.12); border-radius: 9999px;541 background: rgba(14, 14, 16, .72); -webkit-backdrop-filter: blur(20px) saturate(1.4); backdrop-filter: blur(20px) saturate(1.4);542 box-shadow: 0 20px 50px -20px rgba(0,0,0,.6);543}544.fd-lmetal-demo-swatches { display: flex; gap: 8px; margin: 0; padding: 0; }545.fd-lmetal-demo-swatch {546 appearance: none; -webkit-appearance: none; display: block; width: 26px; height: 26px; margin: 0; padding: 0;547 border: 0; border-radius: 50%; cursor: pointer;548 box-shadow: inset 0 0 0 1px rgba(255,255,255,.25), 0 0 0 2px rgba(14,14,16,1), 0 0 0 3px transparent;549 transition: transform 120ms var(--fd-ease-out), box-shadow 240ms var(--fd-ease-out);550}551.fd-lmetal-demo-swatch:hover { transform: scale(1.08); }552.fd-lmetal-demo-swatch:active { transform: scale(0.96); }553.fd-lmetal-demo-swatch[aria-pressed="true"] { box-shadow: inset 0 0 0 1px rgba(255,255,255,.25), 0 0 0 2px rgba(14,14,16,1), 0 0 0 3.5px #F5F5F7; }554.fd-lmetal-demo-swatch:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 4px; }555.fd-lmetal-demo-info { display: flex; flex-direction: column; min-width: 128px; margin: 0; padding: 0 4px; }556.fd-lmetal-demo-finish { margin: 0; padding: 0; color: #F5F5F7; font: 600 13px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0; }557.fd-lmetal-demo-price { margin: 0; padding: 0; color: #A1A1A6; font: 500 12px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; font-variant-numeric: tabular-nums; letter-spacing: 0; }558.fd-lmetal-demo-cta {559 display: inline-flex; align-items: center; height: 40px; margin: 0; padding: 0 20px; border: 0; border-radius: 9999px;560 background: #F5F5F7; color: #0B0B0F; text-decoration: none;561 font: 600 14px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em;562 transition: transform 120ms var(--fd-ease-out), background 120ms var(--fd-ease-out);563}564.fd-lmetal-demo-cta:hover { background: #FFFFFF; }565.fd-lmetal-demo-cta:active { transform: scale(0.96); transition-timing-function: var(--fd-spring); }566.fd-lmetal-demo-cta:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 2px; }567.fd-lmetal-demo-specs { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 24px; width: 100%; max-width: 1280px; margin: 0; padding: 0 8px; }568.fd-lmetal-demo-specs div { margin: 0; padding: 16px 0 0; border-top: 1px solid #2A2A2E; }569.fd-lmetal-demo-specs dt { margin: 0 0 4px; padding: 0; color: #86868B; font: 500 12px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: 0.02em; }570.fd-lmetal-demo-specs dd { margin: 0; padding: 0; color: #F5F5F7; font: 600 17px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em; }571@media (max-width: 720px) {572 .fd-lmetal-demo-panel.fd-lmetal { aspect-ratio: 4 / 5; min-height: 0; }573 .fd-lmetal-demo-bar { flex-wrap: wrap; justify-content: center; border-radius: 24px; padding: 12px; gap: 12px; }574 .fd-lmetal-demo-info { align-items: center; }575 .fd-lmetal-demo-specs { grid-template-columns: 1fr 1fr; }576}577`;A capsule CTA with a live chrome rim that ripples from your touch — plus a zero-JS CSS chrome variant.
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.