Back to all tools
Liquid Glass Generator
Physics-based liquid glass effect using Snell's law refraction, displacement maps, and specular highlights.
Glass Shape
300px
200px
60px
Refraction
80
60
3
1
Appearance
0.3
0.5
4
Inner Shadow
20px
-5px
Glass Tint
6%
Outer Shadow
24px
Preview bg:
Drag the glass to move it over the background
CSS
css
.liquid-glass { width: 300px; height: 200px; border-radius: 60px; isolation: isolate; box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); } .liquid-glass::before { content: ''; position: absolute; inset: 0; border-radius: inherit; box-shadow: inset 0 0 20px -5px #ffffff; background-color: rgba(255, 255, 255, 0.060); pointer-events: none; } .liquid-glass::after { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; backdrop-filter: url(#liquid-glass-filter); -webkit-backdrop-filter: url(#liquid-glass-filter); }
Standalone HTML (copy & paste)
html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Liquid Glass</title> <style> :root { --tint-color: 255, 255, 255; --tint-opacity: 0.060; --shadow-blur: 20px; --shadow-spread: -5px; --shadow-color: #ffffff; --outer-shadow-blur: 24px; --glass-radius: 60px; } * { margin: 0; padding: 0; box-sizing: border-box; } body { width: 100vw; height: 100vh; overflow: hidden; display: flex; align-items: center; justify-content: center; background: url('https://images.unsplash.com/photo-1618221195710-dd6b41faaea6?q=80&w=2000&auto=format&fit=crop') center/cover no-repeat; font-family: -apple-system, BlinkMacSystemFont, system-ui, sans-serif; } .glass { position: absolute; width: 300px; height: 200px; border-radius: var(--glass-radius); cursor: move; isolation: isolate; touch-action: none; box-shadow: 0 4px var(--outer-shadow-blur) rgba(0, 0, 0, 0.18); } .glass::before { content: ''; position: absolute; inset: 0; z-index: 1; border-radius: inherit; box-shadow: inset 0 0 var(--shadow-blur) var(--shadow-spread) var(--shadow-color); background-color: rgba(var(--tint-color), var(--tint-opacity)); pointer-events: none; } .glass::after { content: ''; position: absolute; inset: 0; z-index: -1; border-radius: inherit; backdrop-filter: url(#liquid-glass-filter); -webkit-backdrop-filter: url(#liquid-glass-filter); } </style> </head> <body> <div class="glass" id="glass"></div> <svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style="position:absolute;overflow:hidden" color-interpolation-filters="sRGB"> <defs id="svg-defs"></defs> </svg> <script> // Physics-based liquid glass — Snell's law refraction const SURFACE_FN = (x) => Math.pow(1 - Math.pow(1 - x, 4), 0.25); function calcRefraction(thick, bezel, fn, ior, n) { n = n || 128; const eta = 1 / ior; const p = new Float64Array(n); for (let i = 0; i < n; i++) { const x = i / n, y = fn(x); const dx = x < 1 ? 1e-4 : -1e-4; const dv = (fn(x + dx) - y) / dx; const m = Math.sqrt(dv * dv + 1); const nx = -dv / m, ny = -1 / m; const dot = ny, k = 1 - eta * eta * (1 - dot * dot); if (k < 0) continue; const sq = Math.sqrt(k); const rx = -(eta * dot + sq) * nx; const ry = eta - (eta * dot + sq) * ny; p[i] = rx * ((y * bezel + thick) / ry); } return p; } function genDispMap(w, h, r, bw, prof, md) { const c = document.createElement('canvas'); c.width = w; c.height = h; const ctx = c.getContext('2d'); const img = ctx.createImageData(w, h); const d = img.data; for (let i = 0; i < d.length; i += 4) { d[i]=128; d[i+1]=128; d[i+2]=0; d[i+3]=255; } const rS=r*r, r1=(r+1)**2, rB=Math.max(r-bw,0)**2; const wB=w-r*2, hB=h-r*2, S=prof.length; for (let y1=0; y1<h; y1++) for (let x1=0; x1<w; x1++) { const x=x1<r?x1-r:x1>=w-r?x1-r-wB:0; const y=y1<r?y1-r:y1>=h-r?y1-r-hB:0; const ds=x*x+y*y; if (ds>r1||ds<rB) continue; const di=Math.sqrt(ds), fs=r-di; const op=ds<rS?1:1-(di-Math.sqrt(rS))/(Math.sqrt(r1)-Math.sqrt(rS)); if (op<=0||di===0) continue; const cs=x/di, sn=y/di; const bi=Math.min(((fs/bw)*S)|0,S-1); const dp=prof[bi]||0; const dX=(-cs*dp)/md, dY=(-sn*dp)/md; const idx=(y1*w+x1)*4; d[idx]=(128+dX*127*op+.5)|0; d[idx+1]=(128+dY*127*op+.5)|0; } ctx.putImageData(img,0,0); return c.toDataURL(); } function genSpecMap(w, h, r, bw, angle) { angle = angle ?? Math.PI/3; const c = document.createElement('canvas'); c.width = w; c.height = h; const ctx = c.getContext('2d'); const img = ctx.createImageData(w, h); const d = img.data; d.fill(0); const rS=r*r, r1=(r+1)**2, rB=Math.max(r-bw,0)**2; const wB=w-r*2, hB=h-r*2; const sv=[Math.cos(angle), Math.sin(angle)]; for (let y1=0; y1<h; y1++) for (let x1=0; x1<w; x1++) { const x=x1<r?x1-r:x1>=w-r?x1-r-wB:0; const y=y1<r?y1-r:y1>=h-r?y1-r-hB:0; const ds=x*x+y*y; if (ds>r1||ds<rB) continue; const di=Math.sqrt(ds), fs=r-di; const op=ds<rS?1:1-(di-Math.sqrt(rS))/(Math.sqrt(r1)-Math.sqrt(rS)); if (op<=0||di===0) continue; const cs=x/di, sn=-y/di; const dt=Math.abs(cs*sv[0]+sn*sv[1]); const edge=Math.sqrt(Math.max(0,1-(1-fs)**2)); const co=dt*edge, cl=(255*co)|0, al=(cl*co*op)|0; const idx=(y1*w+x1)*4; d[idx]=cl; d[idx+1]=cl; d[idx+2]=cl; d[idx+3]=al; } ctx.putImageData(img,0,0); return c.toDataURL(); } const glass = document.getElementById('glass'); function rebuild() { const w=300, h=200, r=60; const bw=Math.min(60, r-1, Math.min(w,h)/2-1); const prof=calcRefraction(80, bw, SURFACE_FN, 3, 128); const md=Math.max(...Array.from(prof).map(Math.abs))||1; const du=genDispMap(w,h,r,bw,prof,md); const su=genSpecMap(w,h,r,bw*2.5); const sc=md*1; document.getElementById('svg-defs').innerHTML= '<filter id="liquid-glass-filter" x="0%" y="0%" width="100%" height="100%">'+ '<feGaussianBlur in="SourceGraphic" stdDeviation="0.3" result="b"/>'+ '<feImage href="'+du+'" x="0" y="0" width="'+w+'" height="'+h+'" result="d"/>'+ '<feDisplacementMap in="b" in2="d" scale="'+sc+'" xChannelSelector="R" yChannelSelector="G" result="dp"/>'+ '<feColorMatrix in="dp" type="saturate" values="4" result="ds"/>'+ '<feImage href="'+su+'" x="0" y="0" width="'+w+'" height="'+h+'" result="sl"/>'+ '<feComposite in="ds" in2="sl" operator="in" result="sm"/>'+ '<feComponentTransfer in="sl" result="sf"><feFuncA type="linear" slope="0.5"/></feComponentTransfer>'+ '<feBlend in="sm" in2="dp" mode="normal" result="ws"/>'+ '<feBlend in="sf" in2="ws" mode="normal"/></filter>'; } // Drag support (function(){ let sx,sy; glass.style.left=(innerWidth/2-300/2)+'px'; glass.style.top=(innerHeight/2-200/2)+'px'; glass.addEventListener('pointerdown',function(e){ e.preventDefault(); sx=e.clientX; sy=e.clientY; document.addEventListener('pointermove',mv); document.addEventListener('pointerup',function(){document.removeEventListener('pointermove',mv)},{once:true}); }); function mv(e){e.preventDefault();glass.style.left=(glass.offsetLeft+e.clientX-sx)+'px';glass.style.top=(glass.offsetTop+e.clientY-sy)+'px';sx=e.clientX;sy=e.clientY;} })(); addEventListener('DOMContentLoaded',()=>requestAnimationFrame(()=>requestAnimationFrame(rebuild))); </script> </body> </html>
React Component
jsx
// React Liquid Glass Component (physics-based refraction) // Note: SVG backdrop-filter is Chrome/Chromium only import { useEffect, useRef, useCallback } from "react"; const SURFACE_FN = (x) => Math.pow(1 - Math.pow(1 - x, 4), 0.25); function calcRefraction(thick, bezel, fn, ior, n = 128) { const eta = 1 / ior; const p = new Float64Array(n); for (let i = 0; i < n; i++) { const x = i / n, y = fn(x); const dx = x < 1 ? 1e-4 : -1e-4; const dv = (fn(x + dx) - y) / dx; const m = Math.sqrt(dv * dv + 1); const nx = -dv / m, ny = -1 / m; const dot = ny, k = 1 - eta * eta * (1 - dot * dot); if (k < 0) continue; const sq = Math.sqrt(k); const rx = -(eta * dot + sq) * nx; const ry = eta - (eta * dot + sq) * ny; p[i] = rx * ((y * bezel + thick) / ry); } return p; } function genDispMap(w, h, r, bw, prof, md) { const c = document.createElement("canvas"); c.width = w; c.height = h; const ctx = c.getContext("2d"); const img = ctx.createImageData(w, h); const d = img.data; for (let i = 0; i < d.length; i += 4) { d[i]=128; d[i+1]=128; d[i+2]=0; d[i+3]=255; } const rS=r*r, r1=(r+1)**2, rB=Math.max(r-bw,0)**2; const wB=w-r*2, hB=h-r*2, S=prof.length; for (let y1=0; y1<h; y1++) for (let x1=0; x1<w; x1++) { const x=x1<r?x1-r:x1>=w-r?x1-r-wB:0; const y=y1<r?y1-r:y1>=h-r?y1-r-hB:0; const ds=x*x+y*y; if (ds>r1||ds<rB) continue; const di=Math.sqrt(ds), fs=r-di; const op=ds<rS?1:1-(di-Math.sqrt(rS))/(Math.sqrt(r1)-Math.sqrt(rS)); if (op<=0||di===0) continue; const cs=x/di,sn=y/di,bi=Math.min(((fs/bw)*S)|0,S-1); const dp=prof[bi]||0,dX=(-cs*dp)/md,dY=(-sn*dp)/md; const idx=(y1*w+x1)*4; d[idx]=(128+dX*127*op+.5)|0; d[idx+1]=(128+dY*127*op+.5)|0; } ctx.putImageData(img,0,0); return c.toDataURL(); } function genSpecMap(w, h, r, bw, angle = Math.PI/3) { const c = document.createElement("canvas"); c.width = w; c.height = h; const ctx = c.getContext("2d"); const img = ctx.createImageData(w, h); const d = img.data; d.fill(0); const rS=r*r, r1=(r+1)**2, rB=Math.max(r-bw,0)**2; const wB=w-r*2, hB=h-r*2, sv=[Math.cos(angle),Math.sin(angle)]; for (let y1=0; y1<h; y1++) for (let x1=0; x1<w; x1++) { const x=x1<r?x1-r:x1>=w-r?x1-r-wB:0; const y=y1<r?y1-r:y1>=h-r?y1-r-hB:0; const ds=x*x+y*y; if (ds>r1||ds<rB) continue; const di=Math.sqrt(ds), fs=r-di; const op=ds<rS?1:1-(di-Math.sqrt(rS))/(Math.sqrt(r1)-Math.sqrt(rS)); if (op<=0||di===0) continue; const cs=x/di,sn=-y/di; const dt=Math.abs(cs*sv[0]+sn*sv[1]); const edge=Math.sqrt(Math.max(0,1-(1-fs)**2)); const co=dt*edge,cl=(255*co)|0,al=(cl*co*op)|0; const idx=(y1*w+x1)*4; d[idx]=cl; d[idx+1]=cl; d[idx+2]=cl; d[idx+3]=al; } ctx.putImageData(img,0,0); return c.toDataURL(); } export default function LiquidGlass({ width = 300, height = 200, radius = 60, thickness = 80, bezel = 60, ior = 3, scale = 1, blur = 0.3, specOpacity = 0.5, specSat = 4, shadowColor = "#ffffff", shadowBlur = 20, shadowSpread = -5, tintColor = "255, 255, 255", tintOpacity = 0.060, outerBlur = 24, }) { const defsRef = useRef(null); const rebuild = useCallback(() => { const bw = Math.min(bezel, radius - 1, Math.min(width, height) / 2 - 1); const prof = calcRefraction(thickness, bw, SURFACE_FN, ior, 128); const md = Math.max(...Array.from(prof).map(Math.abs)) || 1; const du = genDispMap(width, height, radius, bw, prof, md); const su = genSpecMap(width, height, radius, bw * 2.5); const sc = md * scale; if (defsRef.current) { defsRef.current.innerHTML = ` <filter id="liquid-glass-filter" x="0%" y="0%" width="100%" height="100%"> <feGaussianBlur in="SourceGraphic" stdDeviation="${blur}" result="b"/> <feImage href="${du}" x="0" y="0" width="${width}" height="${height}" result="d"/> <feDisplacementMap in="b" in2="d" scale="${sc}" xChannelSelector="R" yChannelSelector="G" result="dp"/> <feColorMatrix in="dp" type="saturate" values="${specSat}" result="ds"/> <feImage href="${su}" x="0" y="0" width="${width}" height="${height}" result="sl"/> <feComposite in="ds" in2="sl" operator="in" result="sm"/> <feComponentTransfer in="sl" result="sf"><feFuncA type="linear" slope="${specOpacity}"/></feComponentTransfer> <feBlend in="sm" in2="dp" mode="normal" result="ws"/> <feBlend in="sf" in2="ws" mode="normal"/> </filter>`; } }, [width, height, radius, thickness, bezel, ior, scale, blur, specOpacity, specSat]); useEffect(() => { const t = setTimeout(rebuild, 30); return () => clearTimeout(t); }, [rebuild]); return ( <> <svg xmlns="http://www.w3.org/2000/svg" width="0" height="0" style={{position:"absolute",overflow:"hidden"}} colorInterpolationFilters="sRGB"> <defs ref={defsRef} /> </svg> <div style={{ width, height, borderRadius: radius, isolation: "isolate", boxShadow: `0 4px ${outerBlur}px rgba(0,0,0,0.18)`, position: "relative", }}> <div style={{ position:"absolute", inset:0, zIndex:-1, borderRadius:"inherit", backdropFilter: "url(#liquid-glass-filter)", WebkitBackdropFilter: "url(#liquid-glass-filter)", }} /> <div style={{ position:"absolute", inset:0, zIndex:1, borderRadius:"inherit", boxShadow: `inset 0 0 ${shadowBlur}px ${shadowSpread}px ${shadowColor}`, backgroundColor: `rgba(${tintColor}, ${tintOpacity})`, pointerEvents: "none", }} /> </div> </> ); }