Liquid Metal
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.
Browser support · WebGL2 / WebGL1. Without WebGL the real text gets a CSS chrome gradient via background-clip.
npx shadcn@latest add https://freedesignmd.com/r/liquid-metal-text.json1/**2 * LiquidMetalText — a giant headline cast in flowing chrome. The text is3 * rasterised into a mask, blurred into a bevel height map, and combined with4 * a moving liquid height field; the resulting normals reflect a procedural5 * studio. Mouse tilt slides the highlights across the letters.6 *7 * Props8 * - children: string The headline (also rendered as real, selectable DOM text for a11y/SEO).9 * - as?: "h1" | "h2" | "p" Default "h1".10 * - tint?: "silver" | "gold" | "titanium" | "black-chrome". Default "silver".11 * - weight?: number Font weight. Default 800.12 * - fontFamily?: string Default system display stack.13 * - tilt?: boolean Pointer moves the reflection. Default true.14 * - speed?: number Flow speed. Default 1.15 * CSS: size the text with `font-size` on the root (e.g. clamp()); the canvas matches the DOM text box.16 * CSS vars: --fd-lmtext-fallback.17 *18 * Browser support: WebGL2 / WebGL1. Without WebGL the DOM text shows a CSS19 * chrome gradient via background-clip:text. Reduced motion: still frame.20 */21import { useEffect, useRef, useState } from "react";22import type { CSSProperties, MutableRefObject, 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 sampler2D uMask; // r = crisp coverage, g = blurred (bevel height)195uniform vec2 uRes;196uniform float uTime;197uniform vec2 uTilt;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 liquid(vec2 p){211 float t = uTime * .12;212 vec2 w = vec2(noise(p * .8 + t), noise(p * .8 - t + 4.));213 return noise(p * 1.3 + w * 1.6) + .45 * sin(p.x * 1.1 + p.y * .5 + w.x * 3. + t * 2.5);214}215vec3 studio(vec3 r){216 float el = r.y + uTilt.y * .25;217 float az = atan(r.x, r.z) + uTilt.x * .6;218 vec3 sky = mix(vec3(.26, .27, .3), vec3(.95, .96, .98), smoothstep(-.05, .7, el + .25 * abs(az)));219 vec3 fl = mix(vec3(.02), vec3(.14, .13, .12), smoothstep(-1., -.3, el));220 vec3 c = mix(fl, sky, smoothstep(-.1, -.05, el));221 c += .8 * exp(-pow((el + .03) / .035, 2.));222 c += 1.4 * smoothstep(.08, 0., abs(az + .5) - .18) * smoothstep(.06, 0., abs(el - .35) - .2);223 c += 1.1 * smoothstep(.04, 0., abs(az - .7) - .06) * smoothstep(.05, 0., abs(el - .1) - .35);224 c *= 1. - .8 * smoothstep(.06, 0., abs(az - .25) - .05) * smoothstep(-.05, .1, el);225 return c;226}227void main(){228 vec2 uv = gl_FragCoord.xy / uRes;229 vec2 m = texture2D(uMask, uv).rg;230 if (m.r < .004) { gl_FragColor = vec4(0.); return; }231 vec2 px = 1. / uRes;232 // bevel from the blurred mask233 float bx = texture2D(uMask, uv + vec2(px.x * 2., 0.)).g - texture2D(uMask, uv - vec2(px.x * 2., 0.)).g;234 float by = texture2D(uMask, uv + vec2(0., px.y * 2.)).g - texture2D(uMask, uv - vec2(0., px.y * 2.)).g;235 vec2 p = gl_FragCoord.xy / uRes.y * 3.;236 float e = .01;237 float h = liquid(p);238 vec2 lg = vec2(liquid(p + vec2(e, 0.)) - h, liquid(p + vec2(0., e)) - h) / e;239 vec3 n = normalize(vec3(-bx * 7. - lg.x * .07, -by * 7. - lg.y * .07, 1.));240 vec2 sp = uv - .5;241 vec3 v = normalize(vec3(sp.x * .3, sp.y * 1.1 + .12 + .06 * sin(gl_FragCoord.x / uRes.y * 2. + uTime * .3), -1.));242 vec3 r = reflect(v, n);243 vec3 env = studio(r);244 vec3 tint = uTint < .5 ? vec3(.97) : uTint < 1.5 ? vec3(1., .79, .45) : uTint < 2.5 ? vec3(.8, .82, .86) : vec3(.5);245 float fres = pow(1. - clamp(-dot(n, v), 0., 1.), 5.);246 vec3 col = env * mix(tint, vec3(1.), fres);247 if (uTint > 2.5) col = pow(env, vec3(1.6)) * .6 + .02;248 col = col / (1. + col * .25) * 1.15;249 // thin bright rim on the bevel crest250 col += .18 * smoothstep(.45, .5, m.g) * (1. - smoothstep(.5, .6, m.g));251 float a = m.r;252 gl_FragColor = vec4(col * a, a);253}254`;255 256/* ───────────────────────── component ───────────────────────── */257 258type Tint = "silver" | "gold" | "titanium" | "black-chrome";259const TINTS: Record<Tint, number> = { silver: 0, gold: 1, titanium: 2, "black-chrome": 3 };260const FALLBACK: Record<Tint, string> = {261 silver: "linear-gradient(180deg, #ffffff 0%, #c9ccd2 38%, #3a3c41 50%, #e7e9ec 62%, #8a8e96 100%)",262 gold: "linear-gradient(180deg, #fff6dc 0%, #e2bd72 38%, #4d3712 50%, #f5d996 62%, #a87a33 100%)",263 titanium: "linear-gradient(180deg, #f3f5f8 0%, #aeb5bf 38%, #30343a 50%, #d6dbe2 62%, #737a85 100%)",264 "black-chrome": "linear-gradient(180deg, #9a9da4 0%, #3f4248 38%, #040405 50%, #55585e 62%, #121316 100%)",265};266 267export interface LiquidMetalTextProps {268 children: string;269 as?: "h1" | "h2" | "p";270 tint?: Tint;271 weight?: number;272 fontFamily?: string;273 tilt?: boolean;274 speed?: number;275 className?: string;276 style?: CSSProperties;277}278 279const DEFAULT_FONT = `"SF Pro Display", ui-sans-serif, -apple-system, BlinkMacSystemFont, "Inter", "Segoe UI", Roboto, sans-serif`;280 281export function LiquidMetalText({282 children,283 as: Tag = "h1",284 tint = "silver",285 weight = 800,286 fontFamily = DEFAULT_FONT,287 tilt = true,288 speed = 1,289 className,290 style,291}: LiquidMetalTextProps) {292 const rootRef = useRef<HTMLDivElement>(null);293 const textRef = useRef<HTMLElement>(null);294 const canvasRef = useRef<HTMLCanvasElement>(null);295 const [ready, setReady] = useState(false);296 const st = useRef({ t: 0, tilt: { x: 0, y: 0, tx: 0, ty: 0 }, tint, speed, mask: null as HTMLCanvasElement | null, dirty: false, tex: null as WebGLTexture | null });297 st.current.tint = tint;298 st.current.speed = speed;299 300 const hooks = useRef<StageHooks>({301 setup(s) {302 const { gl } = s;303 st.current.tex = makeTexture(gl, gl.LINEAR, gl.CLAMP_TO_EDGE);304 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array(4));305 gl.enable(gl.BLEND);306 gl.blendFunc(gl.ONE, gl.ONE_MINUS_SRC_ALPHA);307 st.current.dirty = true;308 },309 frame(s, _time, dt) {310 const { gl, u } = s;311 const S = st.current;312 gl.clearColor(0, 0, 0, 0);313 gl.clear(gl.COLOR_BUFFER_BIT);314 gl.activeTexture(gl.TEXTURE0);315 gl.bindTexture(gl.TEXTURE_2D, S.tex);316 if (S.dirty && S.mask && S.mask.width === s.w && S.mask.height === s.h) {317 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);318 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, S.mask);319 gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, false);320 S.dirty = false;321 }322 if (!s.reduced) S.t += dt * S.speed;323 const k = 1 - Math.exp(-dt * 4);324 S.tilt.x += (S.tilt.tx - S.tilt.x) * k;325 S.tilt.y += (S.tilt.ty - S.tilt.y) * k;326 gl.uniform1i(u.uMask, 0);327 gl.uniform2f(u.uRes, s.w, s.h);328 gl.uniform1f(u.uTime, S.t + 5);329 gl.uniform2f(u.uTilt, S.tilt.x, S.tilt.y);330 gl.uniform1f(u.uTint, TINTS[S.tint] ?? 0);331 return true;332 },333 });334 const { failed, kick } = useStage(canvasRef, FRAG, hooks);335 336 // Rasterise the DOM text (same font, size, wrapping) into an RG mask whenever layout changes.337 useEffect(() => {338 const textEl = textRef.current;339 const canvas = canvasRef.current;340 if (!textEl || !canvas || failed) return;341 let raf = 0;342 const build = () => {343 raf = 0;344 const box = textEl.getBoundingClientRect();345 const dpr = Math.min(window.devicePixelRatio || 1, 2);346 const W = Math.max(1, Math.round(box.width * dpr));347 const H = Math.max(1, Math.round(box.height * dpr));348 const cs = getComputedStyle(textEl);349 const fs = parseFloat(cs.fontSize);350 const mask = document.createElement("canvas");351 mask.width = W;352 mask.height = H;353 const ctx = mask.getContext("2d");354 if (!ctx) return;355 ctx.scale(dpr, dpr);356 ctx.font = `${cs.fontStyle} ${cs.fontWeight} ${cs.fontSize} ${cs.fontFamily}`;357 ctx.textBaseline = "alphabetic";358 ctx.fillStyle = "#fff";359 if ("letterSpacing" in ctx) (ctx as CanvasRenderingContext2D & { letterSpacing: string }).letterSpacing = cs.letterSpacing === "normal" ? "0px" : cs.letterSpacing;360 // Walk the text node's line boxes via Range rects so canvas wrapping matches the DOM exactly.361 const node = textEl.firstChild;362 if (node && node.nodeType === 3) {363 const text = node.textContent || "";364 const range = document.createRange();365 let lineTop = -1;366 let lineStart = 0;367 const lines: { s: number; e: number; rect: DOMRect }[] = [];368 for (let i = 0; i < text.length; i++) {369 range.setStart(node, i);370 range.setEnd(node, i + 1);371 const r = range.getClientRects()[0];372 if (!r) continue;373 if (lineTop < 0) lineTop = r.top;374 if (Math.abs(r.top - lineTop) > fs * 0.3) {375 range.setStart(node, lineStart);376 range.setEnd(node, i);377 lines.push({ s: lineStart, e: i, rect: range.getBoundingClientRect() });378 lineStart = i;379 lineTop = r.top;380 }381 }382 range.setStart(node, lineStart);383 range.setEnd(node, text.length);384 lines.push({ s: lineStart, e: text.length, rect: range.getBoundingClientRect() });385 const m = ctx.measureText("Hg");386 const asc = m.fontBoundingBoxAscent ?? fs * 0.8;387 const desc = m.fontBoundingBoxDescent ?? fs * 0.2;388 for (const ln of lines) {389 const str = text.slice(ln.s, ln.e).replace(/\s+$/, "");390 const y = ln.rect.top - box.top + (ln.rect.height - (asc + desc)) / 2 + asc;391 ctx.fillText(str, ln.rect.left - box.left, y);392 }393 }394 // R = crisp, G = blurred copy for the bevel395 const blur = document.createElement("canvas");396 blur.width = W;397 blur.height = H;398 const b = blur.getContext("2d");399 if (!b) return;400 b.filter = `blur(${Math.max(1.5, fs * dpr * 0.035)}px)`;401 b.drawImage(mask, 0, 0);402 const crisp = ctx.getImageData(0, 0, W, H);403 const soft = b.getImageData(0, 0, W, H);404 for (let i = 0; i < crisp.data.length; i += 4) {405 const a = crisp.data[i + 3];406 crisp.data[i] = a;407 crisp.data[i + 1] = soft.data[i + 3];408 crisp.data[i + 2] = 0;409 crisp.data[i + 3] = 255;410 }411 ctx.setTransform(1, 0, 0, 1, 0, 0);412 ctx.putImageData(crisp, 0, 0);413 st.current.mask = mask;414 st.current.dirty = true;415 setReady(true);416 kick.current();417 };418 const schedule = () => {419 if (!raf) raf = requestAnimationFrame(build);420 };421 const ro = new ResizeObserver(schedule);422 ro.observe(textEl);423 document.fonts?.ready.then(schedule);424 schedule();425 return () => {426 cancelAnimationFrame(raf);427 ro.disconnect();428 };429 }, [children, weight, fontFamily, failed, kick]);430 431 useEffect(() => {432 const el = rootRef.current;433 if (!el || !tilt) return;434 const move = (e: PointerEvent) => {435 st.current.tilt.tx = (e.clientX / window.innerWidth - 0.5) * 2;436 st.current.tilt.ty = (e.clientY / window.innerHeight - 0.5) * -2;437 };438 window.addEventListener("pointermove", move, { passive: true });439 return () => window.removeEventListener("pointermove", move);440 }, [tilt]);441 442 return (443 <div444 ref={rootRef}445 className={`fd-lmtext${className ? " " + className : ""}`}446 data-state={failed ? "fallback" : ready ? "ready" : "loading"}447 style={{ ["--fd-lmtext-fallback" as string]: FALLBACK[tint], ...style }}448 >449 <style>{css}</style>450 <Tag ref={textRef as never} className="fd-lmtext-text" style={{ fontWeight: weight, fontFamily }}>451 {children}452 </Tag>453 {!failed && <canvas ref={canvasRef} className="fd-lmtext-canvas" aria-hidden="true" />}454 </div>455 );456}457 458const css = `459.fd-lmtext, .fd-lmtext *, .fd-lmtext *::before, .fd-lmtext *::after { box-sizing: border-box; }460.fd-lmtext { position: relative; display: block; margin: 0; padding: 0; border: 0; background: transparent; }461:where(.fd-lmtext) { font-size: clamp(3rem, 11vw, 10rem); }462.fd-lmtext-text {463 display: block; margin: 0; padding: 0.04em 0 0.1em; border: 0; background: transparent;464 font-size: inherit; line-height: 0.95; text-transform: none; text-decoration: none;465 color: transparent; -webkit-text-fill-color: transparent;466 transition: color 120ms cubic-bezier(0.22, 1, 0.36, 1);467}468.fd-lmtext[data-state="fallback"] .fd-lmtext-text,469.fd-lmtext[data-state="loading"] .fd-lmtext-text {470 background: var(--fd-lmtext-fallback); -webkit-background-clip: text; background-clip: text;471}472:where(.fd-lmtext-text) { letter-spacing: -0.045em; }473.fd-lmtext-text::selection { background: rgba(10,132,255,.35); -webkit-text-fill-color: currentColor; color: #fff; }474.fd-lmtext-canvas { position: absolute; inset: 0; display: block; width: 100%; height: 100%; margin: 0; padding: 0; border: 0; pointer-events: none; }475`;476 477/* ───────────────────────── demo ───────────────────────── */478 479export function Demo() {480 return (481 <section className="fd-lmtext-demo">482 <style>{demoCss}</style>483 <div className="fd-lmtext-demo-top">484 <span>NOVA ATLAS</span>485 <span>New album · 14.11.2026</span>486 </div>487 <div className="fd-lmtext-demo-center">488 <p className="fd-lmtext-demo-pre">The third studio album</p>489 <LiquidMetalText className="fd-lmtext-demo-title">MERCURY</LiquidMetalText>490 <p className="fd-lmtext-demo-sub">Eleven tracks recorded live to tape in Reykjavík. Pre-save now and get the chrome vinyl edition — limited to 3,000 copies.</p>491 <div className="fd-lmtext-demo-actions">492 <a className="fd-lmtext-demo-primary" href="#presave" onClick={(e) => e.preventDefault()}>493 <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M4 2.5v11l9-5.5z" fill="currentColor" /></svg>494 Pre-save495 </a>496 <a className="fd-lmtext-demo-secondary" href="#vinyl" onClick={(e) => e.preventDefault()}>Chrome vinyl · €39</a>497 </div>498 </div>499 <ol className="fd-lmtext-demo-tracks">500 <li><span>01</span>Quicksilver</li>501 <li><span>02</span>Low Orbit</li>502 <li><span>03</span>Hg</li>503 <li><span>04</span>Liquid Hours</li>504 <li><span>05</span>Cold Mirror</li>505 </ol>506 </section>507 );508}509 510const demoCss = `511.fd-lmtext-demo, .fd-lmtext-demo *, .fd-lmtext-demo *::before, .fd-lmtext-demo *::after { box-sizing: border-box; }512.fd-lmtext-demo {513 position: relative; min-height: 100vh; margin: 0; padding: 28px clamp(20px, 4vw, 56px); overflow: hidden;514 display: flex; flex-direction: column; justify-content: space-between; gap: 32px;515 background: radial-gradient(70% 60% at 50% 45%, #1d1e24 0%, #08080A 70%); color: #F5F5F7;516 font: 400 15px/1.5 ui-sans-serif, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;517}518.fd-lmtext-demo-top { display: flex; justify-content: space-between; margin: 0; padding: 0; color: #A1A1A6; font: 600 12px/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; letter-spacing: .14em; text-transform: uppercase; }519.fd-lmtext-demo-center { display: flex; flex-direction: column; align-items: center; text-align: center; margin: 0; padding: 0; }520.fd-lmtext-demo-pre { margin: 0 0 8px; padding: 0; color: #8E8E93; font: 500 15px/1.3 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em; }521.fd-lmtext-demo-title.fd-lmtext { font-size: clamp(4rem, 17vw, 15rem); }522.fd-lmtext-demo-title.fd-lmtext .fd-lmtext-text { letter-spacing: -0.05em; }523.fd-lmtext-demo-sub { max-width: 46ch; margin: 12px 0 28px; 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; }524.fd-lmtext-demo-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 12px; margin: 0; padding: 0; }525.fd-lmtext-demo-primary, .fd-lmtext-demo-secondary {526 display: inline-flex; align-items: center; gap: 8px; height: 48px; margin: 0; padding: 0 24px; border-radius: 9999px; text-decoration: none;527 font: 600 15px/1 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; letter-spacing: -0.01em;528 transition: transform 120ms cubic-bezier(0.22, 1, 0.36, 1), background 120ms cubic-bezier(0.22, 1, 0.36, 1);529}530.fd-lmtext-demo-primary { border: 0; background: #F5F5F7; color: #0B0B0F; }531.fd-lmtext-demo-primary svg { display: block; }532.fd-lmtext-demo-primary:hover { background: #FFFFFF; }533.fd-lmtext-demo-secondary { border: 1px solid rgba(255,255,255,.18); background: rgba(255,255,255,.04); color: #F5F5F7; }534.fd-lmtext-demo-secondary:hover { background: rgba(255,255,255,.1); }535.fd-lmtext-demo-primary:active, .fd-lmtext-demo-secondary:active { transform: scale(0.96); }536.fd-lmtext-demo-primary:focus-visible, .fd-lmtext-demo-secondary:focus-visible { outline: 2px solid rgba(10,132,255,.7); outline-offset: 2px; }537.fd-lmtext-demo-tracks { display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 28px; margin: 0; padding: 0; list-style: none; }538.fd-lmtext-demo-tracks li { margin: 0; padding: 0; list-style: none; color: #C7C7CC; font: 500 13px/1.4 ui-sans-serif, -apple-system, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif; }539.fd-lmtext-demo-tracks span { margin: 0 8px 0 0; padding: 0; color: #6B6B76; font: 500 12px/1 ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; }540`;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.
A raymarched drop of mercury that morphs, merges and reaches for your cursor — a hero centrepiece on a transparent canvas.