liquid glass.
A draggable glass panel that refracts the page along its edge through a displacement map generated from its own shape.
live component, try it
what to expect.
how it works.
An edge that refracts is a displacement map, and the map has to be generated from the panel's own shape or its ring sits on a corner the panel is not wearing. It cannot be one element either: an element carrying a filter is a backdrop root, so its own backdrop-filter samples the filtered copy instead of the page, and no single layer both refracts and frosts. So the refracting layer is a clipped duplicate of the ground, moved by the negative of the panel's offset, and the frost is its sibling. The canonical build puts the map straight on the backdrop, which only Chromium honours: Safari parses the declaration, reports support and paints nothing, so a feature probe would hand it the broken route in silence. This build refracts the duplicate instead, which works in both.
the component.
export interface MapSpec {
width: number;
height: number;
radius: number;
bevel: number;
}
// A third of the bevel keeps the refraction on the edge, not across the panel.
const blurFor = (bevel: number) => Math.max(1, Math.round(bevel / 3));
const round = (n: number) => Math.round(n * 100) / 100;
function ring(w: number, h: number, r: number, bevel: number, paint: string): string {
return (
`<rect x="0" y="0" width="${w}" height="${h}" rx="${r}" ry="${r}"` +
` fill="none" stroke="${paint}" stroke-width="${round(bevel * 2)}" filter="url(#soft)"/>`
);
}
const softFilter = (bevel: number) =>
`<filter id="soft"><feGaussianBlur stdDeviation="${blurFor(bevel)}"/></filter>`;
export function displacementSvg({ width, height, radius, bevel }: MapSpec): string {
const w = round(width);
const h = round(height);
const r = round(radius);
return [
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`,
`<defs>`,
`<linearGradient id="x" x1="0" y1="0" x2="1" y2="0">`,
`<stop offset="0" stop-color="#000000"/>`,
`<stop offset="0.5" stop-color="#800000"/>`,
`<stop offset="1" stop-color="#ff0000"/>`,
`</linearGradient>`,
`<linearGradient id="y" x1="0" y1="0" x2="0" y2="1">`,
`<stop offset="0" stop-color="#000000"/>`,
`<stop offset="0.5" stop-color="#008000"/>`,
`<stop offset="1" stop-color="#00ff00"/>`,
`</linearGradient>`,
softFilter(bevel),
`<mask id="ring">`,
`<rect width="${w}" height="${h}" fill="#000"/>`,
ring(w, h, r, bevel, "#fff"),
`</mask>`,
`</defs>`,
// 128 grey means no displacement; only the ring bends the page.
`<rect width="${w}" height="${h}" fill="#808080"/>`,
`<g mask="url(#ring)" style="isolation:isolate">`,
`<rect width="${w}" height="${h}" fill="url(#x)"/>`,
// Screen blend adds the X ramp (red) and Y ramp (green) without mixing channels.
`<rect width="${w}" height="${h}" fill="url(#y)" style="mix-blend-mode:screen"/>`,
`</g>`,
`</svg>`,
].join("");
}
export function displacementMap(spec: MapSpec): string {
return `data:image/svg+xml,${encodeURIComponent(displacementSvg(spec))}`;
}
// Same ring as an alpha mask, so only the curved band of glass refracts.
export function ringMask({ width, height, radius, bevel }: MapSpec): string {
const w = round(width);
const h = round(height);
const svg =
`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">` +
`<defs>${softFilter(bevel)}</defs>` +
ring(w, h, round(radius), bevel, "#fff") +
`</svg>`;
return `data:image/svg+xml,${encodeURIComponent(svg)}`;
}






