Skip to main content
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>
    </>
  );
}